diff --git a/404.html b/404.html index cb4daa6..a8e6277 100644 --- a/404.html +++ b/404.html @@ -24,10 +24,10 @@ seldom文档 - + -

404

Looks like we've got some broken links.
Take me home
- +

404

How did we get here?
Take me home
+ diff --git a/api-testing/api_case.html b/api-testing/api_case.html index 4699e8e..cc68b71 100644 --- a/api-testing/api_case.html +++ b/api-testing/api_case.html @@ -24,7 +24,7 @@ 支持Excel测试用例 | seldom文档 - +

支持Excel测试用例

seldom > 3.8.0

在编写接口测试用例的时候,有时候测试用例非常简单,比如单接口的测试,不需要登录token,不存在用例数据依赖,也不需要参数加密,此时,使用Excel 文件编写用例更为高效。

seldom支持了这种用例的编写。

编写Excel用例

查看例子open in new window

首先,创建一个Excel文件,格式如下。

nameapimethodheadersparam_typeparamsassertexclude
简单GET接口/getGET{}data{}{}[]
简单POST接口-json参数/postPOST{}json{}{}[]
...

参数说明

字段说明列子
name用例的名称,会在测试报告中展示。
api接口的地址,可以写完整的URL地址, 也可以只定义路径,base_urlconfrun.py例如:http://www.httpbin.org/get or /get
method接口的请求方法,必须大写,不允许为空支持:GETPOSTPUTDELETE
headers请求头,不允许为空,默认为 {},字段必须双引号"例如:{"user-agent": "my-app/0.0.1"}
param_type接口参数类型,必须小写,不允许为空。例如:datajson
params接口参数,不允许为空,默认为 {},字段必须双引号"例如:{"id": 1, "name": "jack"}
assert断言接口返回,允许为空 或 {}例如:{"status": 200, "success": True, "data": [...]}
exclude断言过滤字段,一些特殊的字段会导致断言失败,需要过滤掉。例如:["X-Amzn-Trace-Id", "timestamp"]

confrun.py配置


@@ -158,6 +158,6 @@
 ...
 
 
  • 生成测试报告

- + diff --git a/api-testing/api_object.html b/api-testing/api_object.html index f9eab5b..6270223 100644 --- a/api-testing/api_object.html +++ b/api-testing/api_object.html @@ -24,7 +24,7 @@ API Object | seldom文档 - +

API Object

API Object Models,简称AOM,AOM是一种设计模式,它围绕着将API、路由或功能交互及其相关行为封装在结构良好的对象中。AOM旨在增强API测试和集成的直观性和弹性。在实践中,AOM需要精心设计专门的API对象,以有效地保护用户免受与API 请求、响应、端点交互和身份验证过程相关的复杂性的影响。

seldom 支持AOM, 并且提供了一些好用的功能,辅助你使用AOM.

  • 目录结构如下
mypro/
@@ -73,6 +73,6 @@
 if __name__ == '__main__':
     seldom.main(debug=True, base_url="https://httpbin.org")
 

在用例层调用AuthAPIObject类下面的对象,测试API。

  • AOM 原则

首先,API只允许通过的APIObject进行封装,那么在封装之前可以检索一下是否有封装了,如果有,进一步确认是否满足自己的调用需求,我们一般在测试API的时候一般各种参数验证,当API作为依赖接口调用的时候,一般参数比较少且固定,所以,API在封装的时候要兼顾到这两种情况。

其次,用例层只能通过APIObject的封装调用API,像登录token这种大部分API会用到的信息,可以通过类初始化时传入,后续调用类下面方法的时候就不需要关心的。如果是多个API组成一个场景,也可以再进行一层业务层的封装。

- + diff --git a/api-testing/assert.html b/api-testing/assert.html index ab9a9e0..bb420cc 100644 --- a/api-testing/assert.html +++ b/api-testing/assert.html @@ -24,7 +24,7 @@ 更强大的断言 | seldom文档 - +

更强大的断言

断言接口返回的数据是HTTP接口自动化测试非常重要的工作,提供强大的断言方法可以提高用例的编写效率。

assertJSON

assertJSON() 断言接口返回的某部分数据。

  • 请求参数
{
@@ -125,6 +125,6 @@
         self.assertSchema(assert_data, self.response["args"])
 
 
  • genson: 可以生成jsonschema数据结构和类型(seldom 2.9 新增)。
- + diff --git a/api-testing/more.html b/api-testing/more.html index b34cc5d..f7038f4 100644 --- a/api-testing/more.html +++ b/api-testing/more.html @@ -24,10 +24,10 @@ 更多功能 | seldom文档 - + -

更多功能

har to case

对于不熟悉 Requests 库的人来说,通过Seldom来写接口测试用例还是会有一点难度。于是,seldom 提供了har 文件转 case 的命令。

首先,打开fiddler 工具进行抓包,选中某一个请求。

然后,选择菜单栏:file -> Export Sessions -> Selected Sessions...

选择导出的文件格式。

点击next 保存为demo.har 文件。

最后,通过seldom -h2c 转为demo.py 脚本文件。

> seldom -h2c demo.har
+    

更多功能

har to case

对于不熟悉 Requests 库的人来说,通过Seldom来写接口测试用例还是会有一点难度。于是,seldom 提供了har 文件转 case 的命令。

首先,打开fiddler 工具进行抓包,选中某一个请求。

然后,选择菜单栏:file -> Export Sessions -> Selected Sessions...

选择导出的文件格式。

点击next 保存为demo.har 文件。

最后,通过seldom -h2c 转为demo.py 脚本文件。

> seldom -h2c demo.har
 
 2021-06-14 18:05:50 [INFO] Start to generate testcase.
 2021-06-14 18:05:50 [INFO] created file: ...\demo.py
@@ -40,7 +40,10 @@
         self.url = "http://httpbin.org/post"
 
     def test_case(self):
-        headers = {"User-Agent": "python-requests/2.25.0", "Accept-Encoding": "gzip, deflate", "Accept": "application/json", "Connection": "keep-alive", "Host": "httpbin.org", "Content-Length": "36", "Origin": "http://httpbin.org", "Content-Type": "application/json", "Cookie": "lang=zh"}
+        headers = {"User-Agent": "python-requests/2.25.0", "Accept-Encoding": "gzip, deflate",
+                   "Accept": "application/json", "Connection": "keep-alive", "Host": "httpbin.org",
+                   "Content-Length": "36", "Origin": "http://httpbin.org", "Content-Type": "application/json",
+                   "Cookie": "lang=zh"}
         cookies = {"lang": "zh"}
         self.post(self.url, json={"key1": "value1", "key2": "value2"}, headers=headers, cookies=cookies)
         self.assertStatusCode(200)
@@ -49,15 +52,15 @@
 if __name__ == '__main__':
     seldom.main()
 
-

swagger to case

seldom 3.6 版本支持。

seldom 提供了swaggercase 的命令。 使用 seldom -s2c 命令。

> seldom -s2c swagger.json
+

swagger to case

seldom 3.6 版本支持。

seldom 提供了swaggercase 的命令。 使用 seldom -s2c 命令。

> seldom -s2c swagger.json
 
 2024-03-04 00:02:22 | INFO     | core.py | Start to generate testcase.
 2024-03-04 00:02:22 | INFO     | core.py | created file: ...\swagger.py
 

将swagger文档转为 seldom 自动化测试用例。

import seldom
 
 
-class TestRequest(seldom.TestCase): 
-    
+class TestRequest(seldom.TestCase):
+
     def test_pet_petId_uploadImage_api_post(self):
         url = f"https://petstore.swagger.io/pet/{petId}/uploadImage"
         params = {}
@@ -119,6 +122,7 @@
 curl -X PUT  -H 'Content-Type: application/json' -H 'token: 123' -d '{"key": "value"}' http://httpbin.org/put
 

接口数据依赖

在场景测试中,我们需要利用上一个接口的数据,调用下一个接口。

  • 简单的接口依赖
import seldom
 
+
 class TestRespData(seldom.TestCase):
 
     def test_data_dependency(self):
@@ -132,13 +136,13 @@
         username = self.response["headers"]["X-Account-Fullname"]
         self.post("/post", data={'username': username})
         self.assertStatusCode(200)
-

seldom提供了self.response用于记录上个接口返回的结果,直接拿来用即可。

  • 封装接口依赖
  1. 创建公共模块
# common.py
-from seldom.request import check_response 
+

seldom提供了self.response用于记录上个接口返回的结果,直接拿来用即可。

  • 封装接口依赖
  1. 创建公共模块
# common.py
+from seldom.request import check_response
 from seldom.request import HttpRequest
 
 
 class Common(HttpRequest):
-    
+
     @check_response(
         describe="获取登录用户名",
         status_code=200,
@@ -186,7 +190,7 @@
 2023-02-14 23:51:49 request.py | DEBUG | Execute get_login_user - response:
  {'args': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Account': 'bugmaster', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.28.1', 'X-Amzn-Trace-Id': 'Root=1-63ebae14-1e629b132c21f68e23ffeb33'}, 'origin': '173.248.248.88', 'url': 'http://httpbin.org/get'}
 2023-02-14 23:51:49 request.py | INFO | Execute get_login_user - 获取登录用户名 success!
-

@check_response 专门用于处理封装的方法。

参数说明:

  • describe : 封装方法描述。
  • status_code: 判断接口返回的 HTTP 状态码,默认200
  • ret: 提取接口返回的字段,参考jmespath 提取规则。
  • check: 检查接口返回的字段。参考jmespath 提取规则。
  • debug: 开启debug,打印更多信息。
  1. 引用公共模块
import seldom
+

@check_response 专门用于处理封装的方法。

参数说明:

  • describe: 封装方法描述。
  • status_code: 判断接口返回的 HTTP 状态码,默认200
  • ret: 提取接口返回的字段,参考jmespath 提取规则。
  • check: 检查接口返回的字段。参考jmespath 提取规则。
  • debug: 开启debug,打印更多信息。
  1. 引用公共模块
import seldom
 from common import Common
 
 
@@ -282,8 +286,10 @@
         print(f"jsonpath3 --> {jsonpath3}")
         print(f"jsonpath4 --> {jsonpath4}")
         print(f"jsonpath5 --> {jsonpath5}")
+
+
 ...
-

说明:

  • response: 保存接口返回的数据,可以直接以,字典列表的方式提取。
  • jmespath(): 根据 JMESPath 语法规则,默认提取接口返回的数据,也可指定resposne数据提取。
  • jsonpath(): 根据 JsonPath 语法规则,默认提取接口返回的数据, index指定下标,也可指定resposne数据提取。

运行结果:

2022-05-19 00:57:08 log.py | DEBUG | [response]:
+

说明:

  • response: 保存接口返回的数据,可以直接以,字典列表的方式提取。
  • jmespath(): 根据 JMESPath 语法规则,默认提取接口返回的数据,也可指定resposne数据提取。
  • jsonpath(): 根据 JsonPath 语法规则,默认提取接口返回的数据, index指定下标,也可指定resposne数据提取。

运行结果:

2022-05-19 00:57:08 log.py | DEBUG | [response]:
  {'args': {'age': '18', 'hobby': ['basketball', 'swim'], 'name': 'tom'}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.25.0', 'X-Amzn-Trace-Id': 'Root=1-62852563-2fe77d4b1ce544696af60f10'}, 'origin': '113.87.15.99', 'url': 'http://httpbin.org/get?hobby=basketball&hobby=swim&name=tom&age=18'}
 
 response1 --> tom
@@ -318,10 +324,10 @@
         payload = {"hobby": ["basketball", "swim"], "name": "tom", "age": "18"}
         self.get("/get", params=payload)
         print("response \n", self.response)
-        
+
         schema = genson(self.response)
         print("json Schema \n", schema)
-        
+
         self.assertSchema(schema)
 
  • 运行日志
...
 response
@@ -329,18 +335,17 @@
 
 json Schema
  {'$schema': 'http://json-schema.org/schema#', 'type': 'object', 'properties': {'args': {'type': 'object', 'properties': {'age': {'type': 'string'}, 'hobby': {'type': 'array', 'items': {'type': 'string'}}, 'name': {'type': 'string'}}, 'required': ['age', 'hobby', 'name']}, 'headers': {'type': 'object', 'properties': {'Accept': {'type': 'string'}, 'Accept-Encoding': {'type': 'string'}, 'Host': {'type': 'string'}, 'User-Agent': {'type': 'string'}, 'X-Amzn-Trace-Id': {'type': 'string'}}, 'required': ['Accept', 'Accept-Encoding', 'Host', 'User-Agent', 'X-Amzn-Trace-Id']}, 'origin': {'type': 'string'}, 'url': {'type': 'string'}}, 'required': ['args', 'headers', 'origin', 'url']}
-

mock URL

seldom 3.2.3 支持

seldom 运行允许通过confrun.py文件中mock_url() 配置mock URL映射。

  • confrun.py

配置要映射的mock URL。

...
-
+

mock URL

seldom 3.2.3 支持

seldom 运行允许通过confrun.py文件中mock_url() 配置mock URL映射。

  • confrun.py 配置要映射的mock URL。

 def mock_url():
     """
-
+    mock url
     :return:
     """
     config = {
         "http://httpbin.org/get": "http://127.0.0.1:8000/api/data",
     }
     return config
-
  • test_api.py
import seldom
+
  • test_api.py
import seldom
 
 
 class TestRequest(seldom.TestCase):
@@ -372,7 +377,38 @@
 2023-07-30 14:47:08 | DEBUG    | request.py | [response]:
  [{'item_name': 'apple'}, {'item_name': 'banana'}, {'item_name': 'orange'}, {'item_name': 'watermelon'}, {'item_name': 'grape'}]
 2023-07-30 14:47:08 | INFO     | case.py | 👀 assertStatusCode -> 200.
-

通过日志可以看到 http://httpbin.org/get 替换成为 http://127.0.0.1:8000/api/data 执行。 当你不想mock的时候只需要修改 mock_url() 即可,对于用例来说无影响。

@retry装饰器

@retry() 装饰器用于用法失败充实,例如封装的登录方法,允许API调用失败后再次尝试。

示例如下:

from seldom.request import HttpRequest
+

通过日志可以看到 http://httpbin.org/get 替换成为 http://127.0.0.1:8000/api/data 执行。 当你不想mock的时候只需要修改 mock_url() 即可,对于用例来说无影响。

配置proxies代理

seldom 3.11.0

单个方法设置代理

seldom 支持在每个请求方法中设置代理。

import seldom
+
+
+class TestHttpAssert(seldom.TestCase):
+
+    def test_assert_json(self):
+        """
+        test assertJSON
+        """
+        payload = {"name": "tom", "hobby": ["basketball", "swim"]}
+        proxies = {
+            "https": "http://localhost:1080",
+            "http": "http://localhost:1080",
+        }
+        self.get("/get", params=payload, proxies=proxies)
+

全局设置代理

当我们要所有用例都使用代理时,每个方法都单独设置就很麻烦了,可以使用confrun.py全局设置。

  • 目录结构
├───reports
+├───test_data
+├───test_dir
+│   ├───...
+├───confrun.py # 配置文件
+└───run.py
+
  • confrun.py 配置要映射的mock URL。

+def proxies():
+    """
+    http proxies
+    """
+    proxies_conf = {
+        "https": "http://localhost:1080",
+        "http": "http://localhost:1080",
+    }
+    return proxies_conf
+

通过run.py文件全局运行测试,这里的代理配置将作用于所有请求方法。

@retry装饰器

@retry() 装饰器用于用法失败充实,例如封装的登录方法,允许API调用失败后再次尝试。

示例如下:

from seldom.request import HttpRequest
 from seldom.request import check_response, retry
 
 
@@ -423,6 +459,6 @@
     raise MissingSchema(
 requests.exceptions.MissingSchema: Invalid URL '/error': No scheme supplied. Perhaps you meant https:///error?
 

从运行结果可以看到,调用接口重试了2次,如果仍然错误,抛出异常。

- + diff --git a/api-testing/start.html b/api-testing/start.html index fdb4c51..167fa4c 100644 --- a/api-testing/start.html +++ b/api-testing/start.html @@ -24,7 +24,7 @@ 开始使用 | seldom文档 - +

开始使用

前言

seldom 非常适合个人接口自动化项目,它有以下优势。

  • 可以写更少的代码
  • 提供详细的运行日志
  • 提供专门为接口设计的断言
  • 强大的数据驱动
  • 自动生成HTML/XML测试报告
  • 支持生成随机数据
  • 支持har/swagger文件转case
  • 支持数据库操作

这些是seldom支持的功能,我们只需要集成HTTP接口库,并提供强大的断言即可。seldom 2.0 加入了HTTP接口自动化测试支持。

Seldom 完全兼容 Requestsopen in new window API 如下:

seldomrequests
self.get()requests.get()
self.post()requests.post()
self.put()requests.put()
self.delete()requests.delete()
self.patch()requests.patch()
self.session()requests.session()

Seldom VS Request+unittest

  • unittest + requests 接口自动化示例:
import unittest
@@ -102,6 +102,6 @@
 OK
 2023-02-14 23:37:08 runner.py | SUCCESS | A run the test in debug mode without generating HTML report!
 

通过日志/报告都可以看到详细的HTTP接口调用信息。

- + diff --git a/api-testing/webscocket.html b/api-testing/webscocket.html index cdbe4e6..bbbb011 100644 --- a/api-testing/webscocket.html +++ b/api-testing/webscocket.html @@ -24,7 +24,7 @@ WebSocket | seldom文档 - +

WebSocket

seldom > 3.6.0 支持该功能

有些时间我们需要通过WebSocket实现长连接,很高兴的告诉告诉你seldom支持WebSocket测试了。

WebSocket 生命周期

WebSocket 生命周期中包含几个关键的事件,这些事件允许开发人员在连接的不同阶段执行代码。以下是WebSocket API中定义的主要事件:

  • open: 当WebSocket连接成功建立时触发。这个事件表明客户端与服务器之间的连接已经打开,可以开始数据传输。

  • message: 当客户端接收到服务器发送的消息时触发。这个事件用于处理从服务器接收到的所有消息。

  • error: 当发生错误,导致WebSocket连接关闭之前或连接无法成功建立时触发。这个事件可以用来处理和响应WebSocket过程中出现的任何异常或错误情况。

  • close: 当连接被关闭时触发,无论是客户端还是服务器端主动关闭连接,或是因为某种原因连接被迫关闭。这个事件表明WebSocket连接已经彻底关闭,可以进行清理和后续处理。

seldom测试WebSocket

在seldom中测试WebSocket非常简单。

  • 首先,需要一个WebSocket服务。

通过aiohttp实现websocket_server.py

# websocket_server.py
@@ -107,6 +107,6 @@
 2024-04-05 23:36:36 | SUCCESS  | runner.py | A run the test in debug mode without generating HTML report!
 
 
- + diff --git a/app-testing/appium_lab.html b/app-testing/appium_lab.html index 605359b..3ded1c3 100644 --- a/app-testing/appium_lab.html +++ b/app-testing/appium_lab.html @@ -24,7 +24,7 @@ appium API | seldom文档 - +

appium API

appium API继承 selenium API,所以,操作方法是通用的。在seldom 中,请参考web UI 中的seldom API。

appium 定位

  • 支持定位类型

seldom 支持定位如下,包括selenium/appium。

类型定位**kwargs
selenium/appiumidid_="id"
seleniummamename="name"
selenium/appiumclassclass_name="class"
seleniumtagtag="input"
seleniumlink_textlink_text="文字链接"
seleniumpartial_link_textpartial_link_text="文字链"
selenium/appiumxpathxpath="//*[@id='11']"
seleniumcsscass="input#id"
appiumios_uiautomationios_uiautomation = "xx"
appiumios_predicateios_predicate = "xx"
appiumios_class_chainios_class_chain = "xx"
appiumandroid_uiautomatorandroid_uiautomator = "xx"
appiumandroid_viewtagandroid_viewtag = "xx"
appiumandroid_data_matcherandroid_data_matcher = "xx"
appiumandroid_view_matcherandroid_view_matcher = "xx"
appiumwindows_uiautomationwindows_uiautomation = "xx"
appiumaccessibility_idaccessibility_id = "xx"
appiumimageimage = "xx"
appiumcustomcustom = "xx"
  • 定位用法
import seldom
@@ -75,7 +75,18 @@
 
 if __name__ == '__main__':
     ...
-

AppiumLab 类中分以下几类操作:

Action

Action中提供基本滑动/触摸操作。

from seldom.appium_lab import AppiumLab
+
  • 启动appium server
from seldom.appium_lab.appium_service import AppiumService
+
+if __name__ == '__main__':
+    # 启动 Appium Server
+    app_ser = AppiumService(
+        addr="127.0.0.1",
+        port="4723",
+        use_plugins="images",
+        args=["--allow-cors", "--tmp", "C:\Windows\Temp"])
+    app_ser.start_service()
+

参数说明:

  • addr: appium server 地址, 默认: 127.0.0.1
  • port: appium server 端口, 默认:4723
  • log: 设置 appium server 日志, 默认:appium_server_1734493548.log
  • use_plugins: 设置使用的插件,默认None,不使用。
  • args: 支持添加更多的参数,例如 args=["--allow-cors", "--tmp", "C:\Windows\Temp"]

启动日志:

2024-12-18 11:52:54 | INFO     | appium_service.py | MainThread | 🚀 launch appium server: ['--address', '127.0.0.1', '--port', '4723', '--log', 'D:\\github\\seldomQA\\seldom\\seldom\\appium_lab\\appium_server_1734493974.log', '--use-plugins', 'iamges,ocr', '--allow-cors']
+

AppiumLab 类中分以下几类操作:

Action

Action中提供基本滑动/触摸操作。

from seldom.appium_lab import AppiumLab
 
 appium_lab = AppiumLab()
 # 触摸坐标位
@@ -88,7 +99,9 @@
 appium_lab.swipe_left()
 # 右划
 appium_lab.swipe_right()
-

Switch

Switch中提供基本上下文切换操作。

from seldom.appium_lab import AppiumLab
+# 从x坐标滑动到y坐标
+appium_lab.drag_from_to()
+

Switch

Switch中提供基本上下文切换操作。

from seldom.appium_lab import AppiumLab
 
 appium_lab = AppiumLab()
 
@@ -159,10 +172,6 @@
         self.install_app("/app/path/xxx.apk")
         # 删除app
         self.remove_app("app_id")
-        # 启动app
-        self.launch_app()
-        # 关闭app
-        self.close_app()
         # 如果app正在运行,终止运行
         self.terminate_app("app_id")
         # 如果app未运行,则激活它或者在后台运行
@@ -173,12 +182,10 @@
         # 从指定的设备返回应用程序字符串语言
         language, string = self.app_strings()
         print(language, string)
-        # 启动起app
-        self.reset()
         # 点击图片
         self.click_image("/you/path/xxx.png")
 
-

目前 seldom 集成的 appium API 并不完整,在使用过程中如有问题,欢迎提 issuesopen in new window

- +

目前 seldom 集成的 appium API 并不完整,在使用过程中如有问题,欢迎提 issuesopen in new window

+ diff --git a/app-testing/extensions.html b/app-testing/extensions.html index ac7d26e..a279e59 100644 --- a/app-testing/extensions.html +++ b/app-testing/extensions.html @@ -24,14 +24,14 @@ appium 扩展 | seldom文档 - +

appium 扩展

appium支持扩展,通过扩展来增强appium定位元素的能力。

appium images-plugin

使用此插件支持的-image定位器策略,可以通过Appium指定想要定位的元素的图片文件。

  • 安装Appium images-plugin插件。
> appium plugin install images
 
  • 查看已安装的Appium插件。
> appium plugin list --installed
 ✔ Listing installed plugins
 - images@2.1.8 [installed (npm)]
-
  • 启动Appium server时指定使用OCR插件。
> appium server --address '127.0.0.1' -p 4723  --use-plugins=iamges
+
  • 启动Appium server时指定使用OCR插件。
> appium server --address '127.0.0.1' -p 4723  --use-plugins=images
 
  • 目录结构
├───test_appium_images.py
 └───phone.jpg
 
  • 编写App自动化测试脚本
# test_appium_images.py
@@ -94,48 +94,102 @@
 

根据上面代码示例,打印ocr变量得到一个JSON结构体。

{
   "words": [
     {
-      "text": "mEngine", "confidence": 88.47775268554688,
-      "bbox": {"x0": 86, "y0": 509, "x1": 308, "y1": 560}
+      "text": "mEngine",
+      "confidence": 88.47775268554688,
+      "bbox": {
+        "x0": 86,
+        "y0": 509,
+        "x1": 308,
+        "y1": 560
+      }
     },
     {
-      "text": "Flyme", "confidence": 91.3454818725586,
-       "bbox": {"x0": 316, "y0": 1132, "x1": 420, "y1": 1172}
-      },
+      "text": "Flyme",
+      "confidence": 91.3454818725586,
+      "bbox": {
+        "x0": 316,
+        "y0": 1132,
+        "x1": 420,
+        "y1": 1172
+      }
+    },
     {
-      "text": "A9", "confidence": 34.86248779296875,
-      "bbox": {"x0": 1017, "y0": 2565, "x1": 1078, "y1": 2595}
+      "text": "A9",
+      "confidence": 34.86248779296875,
+      "bbox": {
+        "x0": 1017,
+        "y0": 2565,
+        "x1": 1078,
+        "y1": 2595
+      }
     }
   ],
   "lines": [
     {
-      "text": "mEngine BY Ni0vEh 1 Bl\n\n", "confidence": 21.003677368164062,
-     "bbox": {"x0": 86, "y0": 500, "x1": 674, "y1": 560}
+      "text": "mEngine BY Ni0vEh 1 Bl\n\n",
+      "confidence": 21.003677368164062,
+      "bbox": {
+        "x0": 86,
+        "y0": 500,
+        "x1": 674,
+        "y1": 560
+      }
     },
     {
-      "text": "Flyme\n\n", "confidence": 91.3454818725586,
-     "bbox": {"x0": 316, "y0": 1132, "x1": 420, "y1": 1172}
+      "text": "Flyme\n\n",
+      "confidence": 91.3454818725586,
+      "bbox": {
+        "x0": 316,
+        "y0": 1132,
+        "x1": 420,
+        "y1": 1172
+      }
     },
     {
-      "text": "A9\n", "confidence": 34.86248779296875,
-      "bbox": {"x0": 1017, "y0": 2565, "x1": 1078, "y1": 2595}
+      "text": "A9\n",
+      "confidence": 34.86248779296875,
+      "bbox": {
+        "x0": 1017,
+        "y0": 2565,
+        "x1": 1078,
+        "y1": 2595
+      }
     }
   ],
   "blocks": [
     {
-      "text": "mEngine BY Ni0vEh 1 Bl\n\n", "confidence": 21.003677368164062,
-       "bbox": {"x0": 86, "y0": 500, "x1": 674, "y1": 560}
-      },
+      "text": "mEngine BY Ni0vEh 1 Bl\n\n",
+      "confidence": 21.003677368164062,
+      "bbox": {
+        "x0": 86,
+        "y0": 500,
+        "x1": 674,
+        "y1": 560
+      }
+    },
     {
-      "text": "Flyme\n\n", "confidence": 91.3454818725586,
-      "bbox": {"x0": 316, "y0": 1132, "x1": 420, "y1": 1172}
+      "text": "Flyme\n\n",
+      "confidence": 91.3454818725586,
+      "bbox": {
+        "x0": 316,
+        "y0": 1132,
+        "x1": 420,
+        "y1": 1172
+      }
     },
     {
-      "text": "A9\n", "confidence": 34.86248779296875, 
-      "bbox": {"x0": 1017, "y0": 2565, "x1": 1078, "y1": 2595}
+      "text": "A9\n",
+      "confidence": 34.86248779296875,
+      "bbox": {
+        "x0": 1017,
+        "y0": 2565,
+        "x1": 1078,
+        "y1": 2595
+      }
     }
   ]
 }
-

JSON结构体说明:

  • wrods - Tesseract识别的单个单词的列表。

  • lines - Tesseract识别的文本行的列表。

  • blocks - Tesseract识别连续文本块的列表。

每项都引用一个OCR对象,它们本身包含3个数据:

  • text:识别的文本。
  • confidence:Tesseract对于给定文本的OCR处理结果的置信度(范围在0到100之间)。
  • bbox:发现文本的边界框,边界框标记为x0、x1、y0和y1的值的对象。分别表文本的上下左右坐标位置,其中。这里,x0表示发现文本的左边x坐标,x1表示右边x坐标,y0表示上部y坐标,y1表示下部y坐标。
- +

JSON结构体说明:

每项都引用一个OCR对象,它们本身包含3个数据:

+ diff --git a/app-testing/page_object.html b/app-testing/page_object.html index 837ca3d..fbe4229 100644 --- a/app-testing/page_object.html +++ b/app-testing/page_object.html @@ -24,7 +24,7 @@ Page Object | seldom文档 - +

Page Object

在编写App自动化测试时,推荐使用page object models(简称 PO设计模式)。你可以看到seldom并没有完全封装appium的API,我们可以借助 poium 来实现基于元素的定位。

github: https://github.com/SeldomQA/poium

pip 安装

> pip install poium
@@ -88,6 +88,6 @@
 image = "xx"
 custom = "xx"
 

Element类参数

  • timeout: 设置超时检查次数,默认为5。
  • index: 设置元素索引,当你的定位方式默认匹配到多个元素时,默认返回第1个,即为0.
  • describe: 设置元素描述,默认为undefined, 建议为每个元素增加描述。
- + diff --git a/app-testing/start.html b/app-testing/start.html index 3e9c3b7..72b6fb8 100644 --- a/app-testing/start.html +++ b/app-testing/start.html @@ -24,7 +24,7 @@ app 测试 | seldom文档 - +

app 测试

seldom 3.0 基于appium支持APP测试。

appium 官方网站:https://appium.io/

环境安装

app 的自动化测试环境相比较 web 要复杂一些,请参考appium官方。

  1. 安装node

https://nodejs.org/en/

> node --version
@@ -106,6 +106,6 @@
 .12022-10-03 00:01:40 runner.py | SUCCESS | generated html file: file:///D:\github\seldom\reports\2022_10_03_00_01_23_result.html
 2022-10-03 00:01:40 runner.py | SUCCESS | generated log file: file:///D:\github\seldom\reports\seldom_log.log
 
- + diff --git a/assets/404.html-347f661a.js b/assets/404.html-347f661a.js deleted file mode 100644 index 9abf326..0000000 --- a/assets/404.html-347f661a.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as e,o as c,c as t}from"./app-a06a2d51.js";const _={};function o(r,n){return c(),t("div")}const a=e(_,[["render",o],["__file","404.html.vue"]]);export{a as default}; diff --git a/assets/404.html-0e029f6a.js b/assets/404.html-b6a2d176.js similarity index 63% rename from assets/404.html-0e029f6a.js rename to assets/404.html-b6a2d176.js index 92045ed..1708734 100644 --- a/assets/404.html-0e029f6a.js +++ b/assets/404.html-b6a2d176.js @@ -1 +1 @@ -import{_ as e,o as c,c as t}from"./app-72107ff1.js";const _={};function o(r,n){return c(),t("div")}const a=e(_,[["render",o],["__file","404.html.vue"]]);export{a as default}; +import{_ as e,o as c,c as t}from"./app-9fb6f1b5.js";const _={};function o(r,n){return c(),t("div")}const a=e(_,[["render",o],["__file","404.html.vue"]]);export{a as default}; diff --git a/assets/CHANGES.html-2910bbd8.js b/assets/CHANGES.html-2910bbd8.js new file mode 100644 index 0000000..14a36b2 --- /dev/null +++ b/assets/CHANGES.html-2910bbd8.js @@ -0,0 +1 @@ +import{_ as c,r as s,o as n,c as r,a as e,b as l,d as o,e as d}from"./app-9fb6f1b5.js";const u={},p=e("h1",{id:"版本更新",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#版本更新","aria-hidden":"true"},"#"),l(" 版本更新")],-1),a={id:"seldom-3-x",tabindex:"-1"},_=e("a",{class:"header-anchor",href:"#seldom-3-x","aria-hidden":"true"},"#",-1),h=d("

3.11.0(2024-12-19)

3.10.0(2024-11-11)

",3),g=d("
  • 重要:所有app/web元素定位支持selector模式,详细查看文档。
  • 更新:sleep()增加默认值1s,也支持随机休眠范围:self.seep((1, 3))
  • 更新: appium_lab模块的 Action() 类下面的方法支持自定义休眠时间、间隔时间等。
  • ",3),m=e("code",null,"Steps()",-1),f=e("code",null,"open()",-1),b={href:"https://github.com/SeldomQA/seldom/issues/241",target:"_blank",rel:"noopener noreferrer"},A=e("li",null,[l("告警:"),e("code",null,"type_enter()"),l("添加移除警告,推荐使用"),e("code",null,"type()"),l("。")],-1),k=e("li",null,[l("文档: "),e("ul",null,[e("li",null,"修改playwright使用示例。"),e("li",null,"增加pyAutoGUI使用示例。")])],-1),S=e("p",null,[e("strong",null,"3.9.1(2024-10-10)")],-1),y=d("
  • 更新:脚手架项目模板,增加run.py文件。
  • 修复:生成随机数,获取在线时间接口错误。
  • 修复:datetime.utcnow()在Python 3.12 告警。
  • App测试:
  • API测试:
  • ",5),w=e("code",null,"prompt_value()",-1),x={href:"https://github.com/SeldomQA/seldom/issues/166",target:"_blank",rel:"noopener noreferrer"},v=e("code",null,"action_chains()",-1),T=e("code",null,"ActionChains()",-1),Q={href:"https://github.com/SeldomQA/seldom/issues/119",target:"_blank",rel:"noopener noreferrer"},P=e("code",null,"is_visible()",-1),j={href:"https://github.com/SeldomQA/seldom/issues/62",target:"_blank",rel:"noopener noreferrer"},M=e("li",null,[e("code",null,"Pycharm"),l("右键运行Web UI用例,抛异常提示。")],-1),q={href:"https://github.com/SeldomQA/seldom/issues/31",target:"_blank",rel:"noopener noreferrer"},L={href:"https://github.com/SeldomQA/seldom/issues/174",target:"_blank",rel:"noopener noreferrer"},I=e("li",null,[l("升级:"),e("code",null,"XTestRunner==1.8.0"),l("。")],-1),H=d("

    3.9.0(2024-09-09)

    3.8.1(2024-08-20)

    3.8.0(2024-07-06)

    3.7.1(2024-06-01)

    3.7.0(2024-05-06)

    3.6.0(2024-03-04)

    3.5.0(2024-01-14)

    3.4.1(2023-11-26)

    ",15),R=e("li",null,[l("修复:"),e("code",null,"diff_json()"),l(" 对比特殊数据的异常没有捕捉到。")],-1),C=e("li",null,[e("code",null,"setUpClass()"),l("/"),e("code",null,"tearDownClass()"),l(" 增加异常捕捉,避免报错之后,用例无法统计的问题。")],-1),E=e("code",null,"screenshots()",-1),N={href:"https://github.com/SeldomQA/seldom/issues/201",target:"_blank",rel:"noopener noreferrer"},W=e("li",null,[e("code",null,"open_electron()"),l(" 增加chromedriver_path参数,支持手动指定驱动地址。")],-1),O=e("p",null,[e("strong",null,"3.4.0(2023-11-18)")],-1),U=d("
  • 新增:dependent_func()装饰器,支持用例方法依赖调用,具体使用参考文档。
  • api测试
  • web测试
  • cache操作日志增加 emoji。
  • ",4),B=e("code",null,"diff_json()",-1),X={href:"https://github.com/SeldomQA/seldom/issues/197",target:"_blank",rel:"noopener noreferrer"},D=e("p",null,[e("strong",null,"3.3.0(2023-09-26)")],-1),z=d("
  • web测试
  • 随机数据
  • 增加运行时内嵌(built-in)方法:base_url()driver() - 无需导入,可以在自动化程序任意位置使用这两个方法。
  • 移除parameterized 库的依赖,改为内置。
  • ",4),G=e("code",null,"diff_json()",-1),J=e("code",null,"[{}]",-1),V={href:"https://github.com/SeldomQA/seldom/issues/197",target:"_blank",rel:"noopener noreferrer"},K=e("p",null,[e("strong",null,"3.2.3(2023-07-30)")],-1),Y=d("
  • HTTP自动化
  • Web自动化
  • App测试
  • 优化:seldom_log.log 文件只记录一次运行结果,减少文件大小。
  • ",4),F=e("code",null,"webdriver_manager==4.0.0",-1),Z={href:"https://github.com/SeldomQA/seldom/issues/189",target:"_blank",rel:"noopener noreferrer"},$=e("li",null,[l("其他: 添加 "),e("code",null,"pyproject.toml"),l(" 支持。")],-1),ee=e("li",null,"文档:增加其他库的使用例子。",-1),le=d("

    3.2.2(2023-05-10)

    3.2.1(2023-04-14)

    3.2.0(2023-03-14)

    3.1.3(2023-02-15)

    ",7),oe=e("code",null,"file_data()",-1),ie=e("code",null,"end_line",-1),de={href:"https://github.com/SeldomQA/seldom/issues/163",target:"_blank",rel:"noopener noreferrer"},te=e("li",null,[l("优化:"),e("code",null,"self.assertElement()"),l(" 断言元素时间过长的问题。")],-1),se=e("li",null,[l("优化:"),e("code",null,"self.assertJSON()"),l(" 断言日志,区分告警和错误。")],-1),ce=e("li",null,[l("移除:"),e("code",null,"self.jresponse()"),l(" 方法。")],-1),ne=d("

    3.1.2(internal)

    内部版本:移除了日志打印的 emoji 表情。

    3.1.1(2023-01-03)

    3.1.0(2022-12-15)

    ",6),re=e("li",null,[l("功能:提供 "),e("code",null,"confrun.py"),l(" 运行配置文件,配合 "),e("code",null,"seldom"),l(" 命令使用。")],-1),ue=e("li",null,[l("功能:Web测试,增加 "),e("code",null,"self.get_log()"),l(" 方法。")],-1),pe=e("code",null,"webdriver_manager==3.8.5",-1),ae={href:"https://github.com/SeldomQA/seldom/issues/159",target:"_blank",rel:"noopener noreferrer"},_e={href:"https://github.com/SeldomQA/seldom/issues/158",target:"_blank",rel:"noopener noreferrer"},he=e("li",null,[l("修复:Web测试, "),e("code",null,"self.close()"),l(" 关闭浏览器Bug。")],-1),ge=d("

    3.0.1(2022-11-5)

    3.0.0(2022-10-31)

    3.0.0beta2(2022-10-26)

    ",5),me=e("code",null,"r.text",-1),fe={href:"https://github.com/SeldomQA/seldom/issues/146",target:"_blank",rel:"noopener noreferrer"},be=e("li",null,[l("app测试:感谢 @986379041 "),e("ul",null,[e("li",null,[e("code",null,"install_app()"),l(" 错误")]),e("li",null,[e("code",null,"close_app()"),l(" 错误")])])],-1),Ae=e("code",null,"TestMainExtend",-1),ke=e("code",null,"tester",-1),Se={href:"https://github.com/SeldomQA/seldom/issues/149",target:"_blank",rel:"noopener noreferrer"},ye=e("code",null,"get_month()",-1),we=e("code",null,"get_year()",-1),xe={href:"https://github.com/SeldomQA/seldom/issues/152",target:"_blank",rel:"noopener noreferrer"},ve=e("code",null,"> seldom --clear-cache true",-1),Te={href:"https://github.com/SeldomQA/seldom/issues/153",target:"_blank",rel:"noopener noreferrer"},Qe=e("li",null,[l("其他: "),e("ul",null,[e("li",null,"seldom 运行用例,优化内存使用。")])],-1),Pe=d("

    3.0.0beta1(2022-10-03)

    ",2),je={id:"seldom-2-x",tabindex:"-1"},Me=e("a",{class:"header-anchor",href:"#seldom-2-x","aria-hidden":"true"},"#",-1),qe=d("

    2.10.6/7(2022-09-07)

    2.10.4/5(2022-08-17)

    2.10.3(2022-07-17)

    2.10.2(2022-06-25)

    ",8),Le={href:"https://github.com/SeldomQA/seldom/blob/master/docs/vpdocs/other/other.md",target:"_blank",rel:"noopener noreferrer"},Ie=e("li",null,"功能:测试报告显示断言信息。",-1),He=e("li",null,[l("功能:"),e("code",null,"main()"),l(" 通过"),e("code",null,"open=False"),l("可以控制运行完测试 不自动化打开测试报告。")],-1),Re=e("li",null,[l("增加"),e("code",null,"self.new_browser()"),l(" 可以打开新的浏览器,但只能使用"),e("code",null,"selenium"),l(" 的 API")],-1),Ce=e("code",null,"switch_to_frame_parent",-1),Ee={href:"https://github.com/SeldomQA/seldom/issues/118",target:"_blank",rel:"noopener noreferrer"},Ne=e("code",null,"assertNotElement",-1),We={href:"https://github.com/SeldomQA/seldom/issues/120",target:"_blank",rel:"noopener noreferrer"},Oe=e("li",null,[l("HTTP 测试: "),e("ul",null,[e("li",null,"优化:JSON日志进行格式化打印。")])],-1),Ue=e("p",null,[e("strong",null,"2.10.1(2022-05-30)")],-1),Be=e("ul",null,[e("li",null,"修复:seldom log 问题引起,错误信息无法在控制台打印。")],-1),Xe={href:"https://github.com/SeldomQA/seldom/issues/107",target:"_blank",rel:"noopener noreferrer"},De=e("p",null,[e("strong",null,"2.10.0(2022-05-25)")],-1),ze={href:"https://github.com/SeldomQA/seldom/issues/107",target:"_blank",rel:"noopener noreferrer"},Ge=e("li",null,[l("log方法变更:"),e("code",null,"log.warn()"),l(" -> "),e("code",null,"log.warning()"),l("。")],-1),Je=d("
  • 功能:提供了cache 类来模拟缓存。
  • 功能:@data() 装饰器支持 dict 格式。
  • 功能:self.jresponse() 方法设计不合理,给以废弃提示;可以使用self.jsonpath()/self.jmespath() 替代。
  • 优化:断言方法assertSchema()assertJSON()支持response传参。
  • 优化:@check_response() check检查失败打印response
  • 修复:webdriver_manager 没有设置上限版本,导致webdriver_manager>=3.6.x 报错; 如果使用的 seldom<=2.9 请重新安装webdriver_manager==3.5.2
  • ",6),Ve=e("p",null,[e("strong",null,"2.9.0(2022-04-30)")],-1),Ke=d("
  • seldom log功能:
  • 功能:提供了@check_response() 装饰器,为接口封装提供强大的支持。
  • ",2),Ye=e("code",null,"genson",-1),Fe={href:"https://github.com/SeldomQA/seldom/issues/100",target:"_blank",rel:"noopener noreferrer"},Ze=d("
  • 功能:增加assertInPath() 断言方法。
  • 功能:增加jmespath()方法,方便提取测试数据。
  • 优化:jresponse() 增加对jmespath 语法的支持。
  • 优化:支持self.get()/self.post()/self.put()/self.delete() 返回response对象。
  • ",4),$e=e("p",null,[e("strong",null,"2.8.0(2022-04-16)")],-1),el={href:"https://github.com/SeldomQA/seldom/issues/93",target:"_blank",rel:"noopener noreferrer"},ll={href:"https://github.com/SeldomQA/seldom/issues/94",target:"_blank",rel:"noopener noreferrer"},ol=e("code",null,"sendmail()",-1),il=e("code",null,"delete",-1),dl=e("code",null,"reports/",-1),tl={href:"https://github.com/SeldomQA/seldom/issues/95",target:"_blank",rel:"noopener noreferrer"},sl=e("code",null,"jsonpath",-1),cl=e("code",null,"jresponse()",-1),nl={href:"https://github.com/SeldomQA/seldom/issues/96",target:"_blank",rel:"noopener noreferrer"},rl=e("li",null,[l("功能:创建项目脚手架增加api测试例子:"),e("code",null,"seldom -project mypro"),l(" 。")],-1),ul=e("li",null,"其他: 全新的seldom在线文档:https://seldomqa.github.io/ ,感谢 @nickliya",-1),pl=d("

    2.7.0(2022-03-26)

    2.6.0(2022-03-18)

    2.5.1(2022-02-19)

    ",5),al=e("li",null,"功能:Http接口测试使用日志打印接口信息",-1),_l=e("code",null,"json",-1),hl={href:"https://github.com/SeldomQA/seldom/issues/83",target:"_blank",rel:"noopener noreferrer"},gl=e("li",null,[l("修复:Web UI测试"),e("code",null,"self.Key()"),l(" 无法定位元素的问题")],-1),ml=d("

    2.5.0(2022-01-30)

    2.4.2(2022-01-18)

    2.4.1(2022-01-17)

    2.4.0(2022-01-02)

    ",7),fl=e("li",null,"适配selenium 4.0+ ,适配相关依赖库新版本。",-1),bl=e("li",null,[l("测试用例支持"),e("code",null,"label"),l("标签分类。")],-1),Al={href:"https://github.com/SeldomQA/seldom/issues/79",target:"_blank",rel:"noopener noreferrer"},kl=e("li",null,[l("EdgeChromium浏览器支持"),e("code",null,"headless"),l("模式。")],-1),Sl=e("li",null,[l("Web自动化测试增加元素截图"),e("code",null,"self.element_screenshot()")],-1),yl=e("li",null,"优化HTML测试报告样式。",-1),wl=e("li",null,"优化邮件模板样式。",-1),xl=e("p",null,[e("strong",null,"2.3.3(2021-11-12)")],-1),vl=e("code",null,"assertNotText()",-1),Tl={href:"https://github.com/SeldomQA/seldom/issues/75",target:"_blank",rel:"noopener noreferrer"},Ql=e("code",null,"main()",-1),Pl=e("code",null,"rerun",-1),jl=e("code",null,"save_last_run",-1),Ml={href:"https://github.com/SeldomQA/seldom/issues/76",target:"_blank",rel:"noopener noreferrer"},ql=d("

    2.3.2(2021-11-08)

    2.3.1(2021-11-02)

    2.3.0(2021-10-18)

    2.2.4(2021-09-21)

    ",7),Ll=e("code",null,"url",-1),Il={href:"https://github.com/SeldomQA/seldom/issues/71",target:"_blank",rel:"noopener noreferrer"},Hl={href:"https://github.com/SeldomQA/seldom/issues/72",target:"_blank",rel:"noopener noreferrer"},Rl=e("li",null,"优化HTMLTestRunner, 重跑次数不记录为用例数。",-1),Cl=e("li",null,[l("修复pip安装缺少"),e("code",null,"description.rst"),l(" 问题。")],-1),El=e("p",null,[e("strong",null,"2.2.3(2021-08-27)")],-1),Nl={href:"https://github.com/SeldomQA/seldom/issues/42",target:"_blank",rel:"noopener noreferrer"},Wl=e("code",null,"get_elements()",-1),Ol={href:"https://github.com/SeldomQA/seldom/issues/69",target:"_blank",rel:"noopener noreferrer"},Ul=e("code",null,"colorama",-1),Bl=e("code",null,"emoji",-1),Xl={href:"https://github.com/SeldomQA/seldom/issues/70",target:"_blank",rel:"noopener noreferrer"},Dl=d("

    2.2.2(2021-08-13)

    2.2.1(2021-06-30)

    2.2.0(2021-06-15)

    2.1.1(2021-05-28)

    2.1.0(2021-05-19)

    2.0.1(2021-05-07)

    2.0.0(2021-04-24)

    2.0.0.beta(2021-03-24)

    ",16),zl={id:"seldom-1-x",tabindex:"-1"},Gl=e("a",{class:"header-anchor",href:"#seldom-1-x","aria-hidden":"true"},"#",-1),Jl=d('

    1.10.3(2021-03-23)

    ...

    1.10.2(2021-03-13)

    1.10.1(2021-03-04)

    1.10.0(2021-01-29)

    1.9.0(2020-12-19)

    1.8.0(2020-11-17)

    1.7.2(2020-10-10)

    1.7.0(2020-09-21)

    1.6.0(2020-08-24)

    1.5.6(2020-07-24)

    1.5.5(2020-06-29)

    1.5.4(2020-06-04)

    1.5.3(2020-05-31)

    1.5.2(202x-05-16)

    1.5.1 (2020-05-14)

    1.5.0(2020-04-29)

    1.2.6 (2020-04-22)

    1.2.5(2020-04-13)

    1.2.4(2020-03-19)

    1.2.3(2020-03-11)

    1.2.2(2020-03-03)

    1.2.0(2020-02-01)

    Global launch browser

    1.1.0(2020-01-19)

    selenium grid support Added safari support

    1.0.0(2020-01-04)

    The framework function has been basically improved. I'm glad to release version 1.0

    seldom 0.x

    0.3.6(2019-12-23)

    Add cookie manipulation APIs Optimized element wait

    0.3.5(2019-12-06)

    Added chrome/firefox browser driver download command Driver file path Settings are supported

    0.3.3(2019-11-30)

    add skip case

    0.3.2(2019-11-27)

    Added a switch to display the last rerun result Optimized assertion method

    0.3.0(2019-11-21)

    Update element positioning

    0.2.0(2019-11-17)

    Change the project name to seldom Introducing the poium test library,

    pyse

    0.1.5(2019-11-15)

    0.0.9(2018-03-29)

    Simplifying API calls

    0.0.8(2017-11-23)

    add parameterized Beautification test report

    0.0.7(2016-11-09)

    Re based on unittest.

    0.0.6(2016-04-29)

    add setup.py file, Specification of the installation process, a time to install all dependencies. Delete unnecessary files

    0.0.5

    Increase the support of multiple positioning methods

    0.0.4

    Method to add default to wait. Modify the realization of the individual methods

    0.0.3.1 version update

    0.0.3 version update(2015-09-08)

    0.0.2 version update(2015-09-08)

    ',82);function Vl(Kl,Yl){const t=s("Badge"),i=s("ExternalLinkIcon");return n(),r("div",null,[p,e("h3",a,[_,l(" seldom 3.x "),o(t,{type:"tip",text:"v3",vertical:"top"})]),h,e("ul",null,[g,e("li",null,[l("修复:"),m,l("类的 "),f,l(" 方法默认传url报错 "),e("a",b,[l("#241"),o(i)]),l("。")]),A,k]),S,e("ul",null,[y,e("li",null,[l("Web测试: "),e("ul",null,[e("li",null,[l("增加"),w,l("方法,支持弹窗输入 "),e("a",x,[l("#166"),o(i)]),l("。")]),e("li",null,[l("增加"),v,l("方法,返回Selenium的"),T,l(" 类对象 "),e("a",Q,[l("#119"),o(i)]),l("。")]),e("li",null,[l("增加"),P,l("方法,检查页面元素是否可见 "),e("a",j,[l("#62"),o(i)]),l("。")]),M])]),e("li",null,[l("文档更新: "),e("ul",null,[e("li",null,[l("增加浏览器代理设置示例 "),e("a",q,[l("#31"),o(i)]),l("。")]),e("li",null,[l("操作已打开浏览器示例 "),e("a",L,[l("#174"),o(i)]),l("。")])])]),I]),H,e("ul",null,[R,C,e("li",null,[l("web测试 "),e("ul",null,[e("li",null,[E,l(" 增加images参数,支持传入截图对象 "),e("a",N,[l("#202"),o(i)]),l("。")]),W])])]),O,e("ul",null,[U,e("li",null,[l("修复:"),B,l(" 优化,支持dict深度排序。 "),e("a",X,[l("#197"),o(i)])])]),D,e("ul",null,[z,e("li",null,[l("修复:"),G,l(" 对比 "),J,l(" 数据时报错。 "),e("a",V,[l("#197"),o(i)])])]),K,e("ul",null,[Y,e("li",null,[l("升级:"),F,l(),e("a",Z,[l("#189"),o(i)])]),$,ee]),le,e("ul",null,[e("li",null,[l("功能:"),oe,l(" 增加"),ie,l(" 参数,对于csv/excel文件支持读取到第几行结束。"),e("a",de,[l("#163"),o(i)])]),te,se,ce]),ne,e("ul",null,[re,ue,e("li",null,[l("升级:"),pe,l(" ,支持Mac M1芯片的浏览器驱动。"),e("a",ae,[l("#159"),o(i)])]),e("li",null,[l("修复:seldom-platform平台同步多个项目引起的Bug。"),e("a",_e,[l("#158"),o(i)])]),he]),ge,e("ul",null,[e("li",null,[l("修复: "),e("ul",null,[e("li",null,[l("接口测试: 接口返回文本"),me,l(" 中文乱码问题。"),e("a",fe,[l("#146"),o(i)])]),be])]),e("li",null,[l("功能: "),e("ul",null,[e("li",null,[Ae,l(" 类增加 "),ke,l("参数。 "),e("a",Se,[l("#149"),o(i)])]),e("li",null,[l("生成随机数,增加"),ye,l(" 和 "),we,l("方法。 "),e("a",xe,[l("#152"),o(i)])]),e("li",null,[l("seldom命令增加清除所有缓存。"),ve,l("。 "),e("a",Te,[l("#153"),o(i)])])])]),Qe]),Pe,e("h3",je,[Me,l(" seldom 2.x "),o(t,{type:"tip",text:"v2",vertical:"top"})]),qe,e("p",null,[l("更新:移动模式列表更新,去掉旧设备,增加新设备 "),e("a",Le,[l("link"),o(i)])]),e("ul",null,[Ie,He,e("li",null,[l("Web 测试: "),e("ul",null,[Re,e("li",null,[l("增加"),Ce,l(" 切换到上一级表单,"),e("a",Ee,[l("#118"),o(i)]),l("。")]),e("li",null,[l("优化"),Ne,l(" 执行慢的情况 "),e("a",We,[l("#120"),o(i)])])])]),Oe]),Ue,Be,e("blockquote",null,[e("p",null,[l("2.10.0 为了解决"),e("a",Xe,[l("107"),o(i)]),l(" 问题,我们经过反复的讨论和优化,甚至对相关库XTestRunner做了修改;以为完美解决了这个问题,没想到还是引起了一些严重的错误。为此,我们感到非常沮丧,退回到2.9.0的实现方案。请升级到2.10.1版本。")])]),De,e("ul",null,[e("li",null,[l("seldom log功能: "),e("ul",null,[e("li",null,[l("修复打印日志显示固定文件的问题 "),e("a",ze,[l("107"),o(i)]),l("。")]),Ge])]),Je]),Ve,e("ul",null,[Ke,e("li",null,[l("功能:集成"),Ye,l("库,生成JsonSchema模板 "),e("a",Fe,[l("100"),o(i)]),l(" 。")]),Ze]),$e,e("ul",null,[e("li",null,[l("功能:增加MongoDB 数据库操作 "),e("a",el,[l("93"),o(i)]),l(" 。")]),e("li",null,[l("功能:支持单个用例执行 "),e("a",ll,[l("94"),o(i)]),l(" 。")]),e("li",null,[l("功能:"),ol,l(" 增加"),il,l("参数,发送完邮件删除"),dl,l(" 目录下面的报告和日志文件 "),e("a",tl,[l("95"),o(i)]),l(" 。")]),e("li",null,[l("功能:增加"),sl,l(" 和 "),cl,l(" ,更容易查找json数据 "),e("a",nl,[l("96"),o(i)]),l(" 。")]),rl,ul]),pl,e("ul",null,[al,e("li",null,[l("功能:Http接口测试打印"),_l,l("参数 "),e("a",hl,[l("83"),o(i)])]),gl]),ml,e("ul",null,[fl,bl,e("li",null,[l("接口测试增加打印入参信息 "),e("a",Al,[l("79"),o(i)]),l(" 。")]),kl,Sl,yl,wl]),xl,e("ul",null,[e("li",null,[l("增加 "),vl,l(" 断言方法 "),e("a",Tl,[l("75"),o(i)]),l(" 。")]),e("li",null,[l("修复"),Ql,l("设置"),Pl,l(" 和 "),jl,l("参数,导致用例统计错误 "),e("a",Ml,[l("76"),o(i)]),l(" 。")])]),ql,e("ul",null,[e("li",null,[l("修复HTTP接口测试,指定"),Ll,l("参数错误的问题。"),e("a",Il,[l("71"),o(i)])]),e("li",null,[l("支持发送多人邮件。"),e("a",Hl,[l("72"),o(i)])]),Rl,Cl]),El,e("ul",null,[e("li",null,[l("支持控制台操作步骤显示在HTML报告中。"),e("a",Nl,[l("42"),o(i)])]),e("li",null,[l("修改"),Wl,l("返回空列表。"),e("a",Ol,[l("69"),o(i)])]),e("li",null,[l("修复因为"),Ul,l("/"),Bl,l("导致的编码错误。"),e("a",Xl,[l("70"),o(i)])])]),Dl,e("h3",zl,[Gl,l(" seldom 1.x "),o(t,{type:"tip",text:"v1",vertical:"top"})]),Jl])}const Zl=c(u,[["render",Vl],["__file","CHANGES.html.vue"]]);export{Zl as default}; diff --git a/assets/CHANGES.html-5bee8a25.js b/assets/CHANGES.html-5bee8a25.js deleted file mode 100644 index 28dafac..0000000 --- a/assets/CHANGES.html-5bee8a25.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as c,r as s,o as n,c as r,a as e,b as l,d as o,e as d}from"./app-a06a2d51.js";const u={},p=e("h1",{id:"版本更新",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#版本更新","aria-hidden":"true"},"#"),l(" 版本更新")],-1),a={id:"seldom-3-x",tabindex:"-1"},_=e("a",{class:"header-anchor",href:"#seldom-3-x","aria-hidden":"true"},"#",-1),h=e("p",null,[e("strong",null,"3.10.0(2024-11-11)")],-1),g=d("
  • 重要:所有app/web元素定位支持selector模式,详细查看文档。
  • 更新:sleep()增加默认值1s,也支持随机休眠范围:self.seep((1, 3))
  • 更新: appium_lab模块的 Action() 类下面的方法支持自定义休眠时间、间隔时间等。
  • ",3),m=e("code",null,"Steps()",-1),f=e("code",null,"open()",-1),b={href:"https://github.com/SeldomQA/seldom/issues/241",target:"_blank",rel:"noopener noreferrer"},A=e("li",null,[l("告警:"),e("code",null,"type_enter()"),l("添加移除警告,推荐使用"),e("code",null,"type()"),l("。")],-1),k=e("li",null,[l("文档: "),e("ul",null,[e("li",null,"修改playwright使用示例。"),e("li",null,"增加pyAutoGUI使用示例。")])],-1),S=e("p",null,[e("strong",null,"3.9.1(2024-10-10)")],-1),y=d("
  • 更新:脚手架项目模板,增加run.py文件。
  • 修复:生成随机数,获取在线时间接口错误。
  • 修复:datetime.utcnow()在Python 3.12 告警。
  • App测试:
  • API测试:
  • ",5),w=e("code",null,"prompt_value()",-1),x={href:"https://github.com/SeldomQA/seldom/issues/166",target:"_blank",rel:"noopener noreferrer"},v=e("code",null,"action_chains()",-1),T=e("code",null,"ActionChains()",-1),Q={href:"https://github.com/SeldomQA/seldom/issues/119",target:"_blank",rel:"noopener noreferrer"},P=e("code",null,"is_visible()",-1),j={href:"https://github.com/SeldomQA/seldom/issues/62",target:"_blank",rel:"noopener noreferrer"},M=e("li",null,[e("code",null,"Pycharm"),l("右键运行Web UI用例,抛异常提示。")],-1),q={href:"https://github.com/SeldomQA/seldom/issues/31",target:"_blank",rel:"noopener noreferrer"},L={href:"https://github.com/SeldomQA/seldom/issues/174",target:"_blank",rel:"noopener noreferrer"},I=e("li",null,[l("升级:"),e("code",null,"XTestRunner==1.8.0"),l("。")],-1),H=d("

    3.9.0(2024-09-09)

    3.8.1(2024-08-20)

    3.8.0(2024-07-06)

    3.7.1(2024-06-01)

    3.7.0(2024-05-06)

    3.6.0(2024-03-04)

    3.5.0(2024-01-14)

    3.4.1(2023-11-26)

    ",15),R=e("li",null,[l("修复:"),e("code",null,"diff_json()"),l(" 对比特殊数据的异常没有捕捉到。")],-1),C=e("li",null,[e("code",null,"setUpClass()"),l("/"),e("code",null,"tearDownClass()"),l(" 增加异常捕捉,避免报错之后,用例无法统计的问题。")],-1),E=e("code",null,"screenshots()",-1),N={href:"https://github.com/SeldomQA/seldom/issues/201",target:"_blank",rel:"noopener noreferrer"},W=e("li",null,[e("code",null,"open_electron()"),l(" 增加chromedriver_path参数,支持手动指定驱动地址。")],-1),O=e("p",null,[e("strong",null,"3.4.0(2023-11-18)")],-1),U=d("
  • 新增:dependent_func()装饰器,支持用例方法依赖调用,具体使用参考文档。
  • api测试
  • web测试
  • cache操作日志增加 emoji。
  • ",4),B=e("code",null,"diff_json()",-1),X={href:"https://github.com/SeldomQA/seldom/issues/197",target:"_blank",rel:"noopener noreferrer"},D=e("p",null,[e("strong",null,"3.3.0(2023-09-26)")],-1),z=d("
  • web测试
  • 随机数据
  • 增加运行时内嵌(built-in)方法:base_url()driver() - 无需导入,可以在自动化程序任意位置使用这两个方法。
  • 移除parameterized 库的依赖,改为内置。
  • ",4),G=e("code",null,"diff_json()",-1),J=e("code",null,"[{}]",-1),V={href:"https://github.com/SeldomQA/seldom/issues/197",target:"_blank",rel:"noopener noreferrer"},K=e("p",null,[e("strong",null,"3.2.3(2023-07-30)")],-1),Y=d("
  • HTTP自动化
  • Web自动化
  • App测试
  • 优化:seldom_log.log 文件只记录一次运行结果,减少文件大小。
  • ",4),F=e("code",null,"webdriver_manager==4.0.0",-1),Z={href:"https://github.com/SeldomQA/seldom/issues/189",target:"_blank",rel:"noopener noreferrer"},$=e("li",null,[l("其他: 添加 "),e("code",null,"pyproject.toml"),l(" 支持。")],-1),ee=e("li",null,"文档:增加其他库的使用例子。",-1),le=d("

    3.2.2(2023-05-10)

    3.2.1(2023-04-14)

    3.2.0(2023-03-14)

    3.1.3(2023-02-15)

    ",7),oe=e("code",null,"file_data()",-1),ie=e("code",null,"end_line",-1),de={href:"https://github.com/SeldomQA/seldom/issues/163",target:"_blank",rel:"noopener noreferrer"},te=e("li",null,[l("优化:"),e("code",null,"self.assertElement()"),l(" 断言元素时间过长的问题。")],-1),se=e("li",null,[l("优化:"),e("code",null,"self.assertJSON()"),l(" 断言日志,区分告警和错误。")],-1),ce=e("li",null,[l("移除:"),e("code",null,"self.jresponse()"),l(" 方法。")],-1),ne=d("

    3.1.2(internal)

    内部版本:移除了日志打印的 emoji 表情。

    3.1.1(2023-01-03)

    3.1.0(2022-12-15)

    ",6),re=e("li",null,[l("功能:提供 "),e("code",null,"confrun.py"),l(" 运行配置文件,配合 "),e("code",null,"seldom"),l(" 命令使用。")],-1),ue=e("li",null,[l("功能:Web测试,增加 "),e("code",null,"self.get_log()"),l(" 方法。")],-1),pe=e("code",null,"webdriver_manager==3.8.5",-1),ae={href:"https://github.com/SeldomQA/seldom/issues/159",target:"_blank",rel:"noopener noreferrer"},_e={href:"https://github.com/SeldomQA/seldom/issues/158",target:"_blank",rel:"noopener noreferrer"},he=e("li",null,[l("修复:Web测试, "),e("code",null,"self.close()"),l(" 关闭浏览器Bug。")],-1),ge=d("

    3.0.1(2022-11-5)

    3.0.0(2022-10-31)

    3.0.0beta2(2022-10-26)

    ",5),me=e("code",null,"r.text",-1),fe={href:"https://github.com/SeldomQA/seldom/issues/146",target:"_blank",rel:"noopener noreferrer"},be=e("li",null,[l("app测试:感谢 @986379041 "),e("ul",null,[e("li",null,[e("code",null,"install_app()"),l(" 错误")]),e("li",null,[e("code",null,"close_app()"),l(" 错误")])])],-1),Ae=e("code",null,"TestMainExtend",-1),ke=e("code",null,"tester",-1),Se={href:"https://github.com/SeldomQA/seldom/issues/149",target:"_blank",rel:"noopener noreferrer"},ye=e("code",null,"get_month()",-1),we=e("code",null,"get_year()",-1),xe={href:"https://github.com/SeldomQA/seldom/issues/152",target:"_blank",rel:"noopener noreferrer"},ve=e("code",null,"> seldom --clear-cache true",-1),Te={href:"https://github.com/SeldomQA/seldom/issues/153",target:"_blank",rel:"noopener noreferrer"},Qe=e("li",null,[l("其他: "),e("ul",null,[e("li",null,"seldom 运行用例,优化内存使用。")])],-1),Pe=d("

    3.0.0beta1(2022-10-03)

    ",2),je={id:"seldom-2-x",tabindex:"-1"},Me=e("a",{class:"header-anchor",href:"#seldom-2-x","aria-hidden":"true"},"#",-1),qe=d("

    2.10.6/7(2022-09-07)

    2.10.4/5(2022-08-17)

    2.10.3(2022-07-17)

    2.10.2(2022-06-25)

    ",8),Le={href:"https://github.com/SeldomQA/seldom/blob/master/docs/vpdocs/other/other.md",target:"_blank",rel:"noopener noreferrer"},Ie=e("li",null,"功能:测试报告显示断言信息。",-1),He=e("li",null,[l("功能:"),e("code",null,"main()"),l(" 通过"),e("code",null,"open=False"),l("可以控制运行完测试 不自动化打开测试报告。")],-1),Re=e("li",null,[l("增加"),e("code",null,"self.new_browser()"),l(" 可以打开新的浏览器,但只能使用"),e("code",null,"selenium"),l(" 的 API")],-1),Ce=e("code",null,"switch_to_frame_parent",-1),Ee={href:"https://github.com/SeldomQA/seldom/issues/118",target:"_blank",rel:"noopener noreferrer"},Ne=e("code",null,"assertNotElement",-1),We={href:"https://github.com/SeldomQA/seldom/issues/120",target:"_blank",rel:"noopener noreferrer"},Oe=e("li",null,[l("HTTP 测试: "),e("ul",null,[e("li",null,"优化:JSON日志进行格式化打印。")])],-1),Ue=e("p",null,[e("strong",null,"2.10.1(2022-05-30)")],-1),Be=e("ul",null,[e("li",null,"修复:seldom log 问题引起,错误信息无法在控制台打印。")],-1),Xe={href:"https://github.com/SeldomQA/seldom/issues/107",target:"_blank",rel:"noopener noreferrer"},De=e("p",null,[e("strong",null,"2.10.0(2022-05-25)")],-1),ze={href:"https://github.com/SeldomQA/seldom/issues/107",target:"_blank",rel:"noopener noreferrer"},Ge=e("li",null,[l("log方法变更:"),e("code",null,"log.warn()"),l(" -> "),e("code",null,"log.warning()"),l("。")],-1),Je=d("
  • 功能:提供了cache 类来模拟缓存。
  • 功能:@data() 装饰器支持 dict 格式。
  • 功能:self.jresponse() 方法设计不合理,给以废弃提示;可以使用self.jsonpath()/self.jmespath() 替代。
  • 优化:断言方法assertSchema()assertJSON()支持response传参。
  • 优化:@check_response() check检查失败打印response
  • 修复:webdriver_manager 没有设置上限版本,导致webdriver_manager>=3.6.x 报错; 如果使用的 seldom<=2.9 请重新安装webdriver_manager==3.5.2
  • ",6),Ve=e("p",null,[e("strong",null,"2.9.0(2022-04-30)")],-1),Ke=d("
  • seldom log功能:
  • 功能:提供了@check_response() 装饰器,为接口封装提供强大的支持。
  • ",2),Ye=e("code",null,"genson",-1),Fe={href:"https://github.com/SeldomQA/seldom/issues/100",target:"_blank",rel:"noopener noreferrer"},Ze=d("
  • 功能:增加assertInPath() 断言方法。
  • 功能:增加jmespath()方法,方便提取测试数据。
  • 优化:jresponse() 增加对jmespath 语法的支持。
  • 优化:支持self.get()/self.post()/self.put()/self.delete() 返回response对象。
  • ",4),$e=e("p",null,[e("strong",null,"2.8.0(2022-04-16)")],-1),el={href:"https://github.com/SeldomQA/seldom/issues/93",target:"_blank",rel:"noopener noreferrer"},ll={href:"https://github.com/SeldomQA/seldom/issues/94",target:"_blank",rel:"noopener noreferrer"},ol=e("code",null,"sendmail()",-1),il=e("code",null,"delete",-1),dl=e("code",null,"reports/",-1),tl={href:"https://github.com/SeldomQA/seldom/issues/95",target:"_blank",rel:"noopener noreferrer"},sl=e("code",null,"jsonpath",-1),cl=e("code",null,"jresponse()",-1),nl={href:"https://github.com/SeldomQA/seldom/issues/96",target:"_blank",rel:"noopener noreferrer"},rl=e("li",null,[l("功能:创建项目脚手架增加api测试例子:"),e("code",null,"seldom -project mypro"),l(" 。")],-1),ul=e("li",null,"其他: 全新的seldom在线文档:https://seldomqa.github.io/ ,感谢 @nickliya",-1),pl=d("

    2.7.0(2022-03-26)

    2.6.0(2022-03-18)

    2.5.1(2022-02-19)

    ",5),al=e("li",null,"功能:Http接口测试使用日志打印接口信息",-1),_l=e("code",null,"json",-1),hl={href:"https://github.com/SeldomQA/seldom/issues/83",target:"_blank",rel:"noopener noreferrer"},gl=e("li",null,[l("修复:Web UI测试"),e("code",null,"self.Key()"),l(" 无法定位元素的问题")],-1),ml=d("

    2.5.0(2022-01-30)

    2.4.2(2022-01-18)

    2.4.1(2022-01-17)

    2.4.0(2022-01-02)

    ",7),fl=e("li",null,"适配selenium 4.0+ ,适配相关依赖库新版本。",-1),bl=e("li",null,[l("测试用例支持"),e("code",null,"label"),l("标签分类。")],-1),Al={href:"https://github.com/SeldomQA/seldom/issues/79",target:"_blank",rel:"noopener noreferrer"},kl=e("li",null,[l("EdgeChromium浏览器支持"),e("code",null,"headless"),l("模式。")],-1),Sl=e("li",null,[l("Web自动化测试增加元素截图"),e("code",null,"self.element_screenshot()")],-1),yl=e("li",null,"优化HTML测试报告样式。",-1),wl=e("li",null,"优化邮件模板样式。",-1),xl=e("p",null,[e("strong",null,"2.3.3(2021-11-12)")],-1),vl=e("code",null,"assertNotText()",-1),Tl={href:"https://github.com/SeldomQA/seldom/issues/75",target:"_blank",rel:"noopener noreferrer"},Ql=e("code",null,"main()",-1),Pl=e("code",null,"rerun",-1),jl=e("code",null,"save_last_run",-1),Ml={href:"https://github.com/SeldomQA/seldom/issues/76",target:"_blank",rel:"noopener noreferrer"},ql=d("

    2.3.2(2021-11-08)

    2.3.1(2021-11-02)

    2.3.0(2021-10-18)

    2.2.4(2021-09-21)

    ",7),Ll=e("code",null,"url",-1),Il={href:"https://github.com/SeldomQA/seldom/issues/71",target:"_blank",rel:"noopener noreferrer"},Hl={href:"https://github.com/SeldomQA/seldom/issues/72",target:"_blank",rel:"noopener noreferrer"},Rl=e("li",null,"优化HTMLTestRunner, 重跑次数不记录为用例数。",-1),Cl=e("li",null,[l("修复pip安装缺少"),e("code",null,"description.rst"),l(" 问题。")],-1),El=e("p",null,[e("strong",null,"2.2.3(2021-08-27)")],-1),Nl={href:"https://github.com/SeldomQA/seldom/issues/42",target:"_blank",rel:"noopener noreferrer"},Wl=e("code",null,"get_elements()",-1),Ol={href:"https://github.com/SeldomQA/seldom/issues/69",target:"_blank",rel:"noopener noreferrer"},Ul=e("code",null,"colorama",-1),Bl=e("code",null,"emoji",-1),Xl={href:"https://github.com/SeldomQA/seldom/issues/70",target:"_blank",rel:"noopener noreferrer"},Dl=d("

    2.2.2(2021-08-13)

    2.2.1(2021-06-30)

    2.2.0(2021-06-15)

    2.1.1(2021-05-28)

    2.1.0(2021-05-19)

    2.0.1(2021-05-07)

    2.0.0(2021-04-24)

    2.0.0.beta(2021-03-24)

    ",16),zl={id:"seldom-1-x",tabindex:"-1"},Gl=e("a",{class:"header-anchor",href:"#seldom-1-x","aria-hidden":"true"},"#",-1),Jl=d('

    1.10.3(2021-03-23)

    ...

    1.10.2(2021-03-13)

    1.10.1(2021-03-04)

    1.10.0(2021-01-29)

    1.9.0(2020-12-19)

    1.8.0(2020-11-17)

    1.7.2(2020-10-10)

    1.7.0(2020-09-21)

    1.6.0(2020-08-24)

    1.5.6(2020-07-24)

    1.5.5(2020-06-29)

    1.5.4(2020-06-04)

    1.5.3(2020-05-31)

    1.5.2(202x-05-16)

    1.5.1 (2020-05-14)

    1.5.0(2020-04-29)

    1.2.6 (2020-04-22)

    1.2.5(2020-04-13)

    1.2.4(2020-03-19)

    1.2.3(2020-03-11)

    1.2.2(2020-03-03)

    1.2.0(2020-02-01)

    Global launch browser

    1.1.0(2020-01-19)

    selenium grid support Added safari support

    1.0.0(2020-01-04)

    The framework function has been basically improved. I'm glad to release version 1.0

    seldom 0.x

    0.3.6(2019-12-23)

    Add cookie manipulation APIs Optimized element wait

    0.3.5(2019-12-06)

    Added chrome/firefox browser driver download command Driver file path Settings are supported

    0.3.3(2019-11-30)

    add skip case

    0.3.2(2019-11-27)

    Added a switch to display the last rerun result Optimized assertion method

    0.3.0(2019-11-21)

    Update element positioning

    0.2.0(2019-11-17)

    Change the project name to seldom Introducing the poium test library,

    pyse

    0.1.5(2019-11-15)

    0.0.9(2018-03-29)

    Simplifying API calls

    0.0.8(2017-11-23)

    add parameterized Beautification test report

    0.0.7(2016-11-09)

    Re based on unittest.

    0.0.6(2016-04-29)

    add setup.py file, Specification of the installation process, a time to install all dependencies. Delete unnecessary files

    0.0.5

    Increase the support of multiple positioning methods

    0.0.4

    Method to add default to wait. Modify the realization of the individual methods

    0.0.3.1 version update

    0.0.3 version update(2015-09-08)

    0.0.2 version update(2015-09-08)

    ',82);function Vl(Kl,Yl){const t=s("Badge"),i=s("ExternalLinkIcon");return n(),r("div",null,[p,e("h3",a,[_,l(" seldom 3.x "),o(t,{type:"tip",text:"v3",vertical:"top"})]),h,e("ul",null,[g,e("li",null,[l("修复:"),m,l("类的 "),f,l(" 方法默认传url报错 "),e("a",b,[l("#241"),o(i)]),l("。")]),A,k]),S,e("ul",null,[y,e("li",null,[l("Web测试: "),e("ul",null,[e("li",null,[l("增加"),w,l("方法,支持弹窗输入 "),e("a",x,[l("#166"),o(i)]),l("。")]),e("li",null,[l("增加"),v,l("方法,返回Selenium的"),T,l(" 类对象 "),e("a",Q,[l("#119"),o(i)]),l("。")]),e("li",null,[l("增加"),P,l("方法,检查页面元素是否可见 "),e("a",j,[l("#62"),o(i)]),l("。")]),M])]),e("li",null,[l("文档更新: "),e("ul",null,[e("li",null,[l("增加浏览器代理设置示例 "),e("a",q,[l("#31"),o(i)]),l("。")]),e("li",null,[l("操作已打开浏览器示例 "),e("a",L,[l("#174"),o(i)]),l("。")])])]),I]),H,e("ul",null,[R,C,e("li",null,[l("web测试 "),e("ul",null,[e("li",null,[E,l(" 增加images参数,支持传入截图对象 "),e("a",N,[l("#202"),o(i)]),l("。")]),W])])]),O,e("ul",null,[U,e("li",null,[l("修复:"),B,l(" 优化,支持dict深度排序。 "),e("a",X,[l("#197"),o(i)])])]),D,e("ul",null,[z,e("li",null,[l("修复:"),G,l(" 对比 "),J,l(" 数据时报错。 "),e("a",V,[l("#197"),o(i)])])]),K,e("ul",null,[Y,e("li",null,[l("升级:"),F,l(),e("a",Z,[l("#189"),o(i)])]),$,ee]),le,e("ul",null,[e("li",null,[l("功能:"),oe,l(" 增加"),ie,l(" 参数,对于csv/excel文件支持读取到第几行结束。"),e("a",de,[l("#163"),o(i)])]),te,se,ce]),ne,e("ul",null,[re,ue,e("li",null,[l("升级:"),pe,l(" ,支持Mac M1芯片的浏览器驱动。"),e("a",ae,[l("#159"),o(i)])]),e("li",null,[l("修复:seldom-platform平台同步多个项目引起的Bug。"),e("a",_e,[l("#158"),o(i)])]),he]),ge,e("ul",null,[e("li",null,[l("修复: "),e("ul",null,[e("li",null,[l("接口测试: 接口返回文本"),me,l(" 中文乱码问题。"),e("a",fe,[l("#146"),o(i)])]),be])]),e("li",null,[l("功能: "),e("ul",null,[e("li",null,[Ae,l(" 类增加 "),ke,l("参数。 "),e("a",Se,[l("#149"),o(i)])]),e("li",null,[l("生成随机数,增加"),ye,l(" 和 "),we,l("方法。 "),e("a",xe,[l("#152"),o(i)])]),e("li",null,[l("seldom命令增加清除所有缓存。"),ve,l("。 "),e("a",Te,[l("#153"),o(i)])])])]),Qe]),Pe,e("h3",je,[Me,l(" seldom 2.x "),o(t,{type:"tip",text:"v2",vertical:"top"})]),qe,e("p",null,[l("更新:移动模式列表更新,去掉旧设备,增加新设备 "),e("a",Le,[l("link"),o(i)])]),e("ul",null,[Ie,He,e("li",null,[l("Web 测试: "),e("ul",null,[Re,e("li",null,[l("增加"),Ce,l(" 切换到上一级表单,"),e("a",Ee,[l("#118"),o(i)]),l("。")]),e("li",null,[l("优化"),Ne,l(" 执行慢的情况 "),e("a",We,[l("#120"),o(i)])])])]),Oe]),Ue,Be,e("blockquote",null,[e("p",null,[l("2.10.0 为了解决"),e("a",Xe,[l("107"),o(i)]),l(" 问题,我们经过反复的讨论和优化,甚至对相关库XTestRunner做了修改;以为完美解决了这个问题,没想到还是引起了一些严重的错误。为此,我们感到非常沮丧,退回到2.9.0的实现方案。请升级到2.10.1版本。")])]),De,e("ul",null,[e("li",null,[l("seldom log功能: "),e("ul",null,[e("li",null,[l("修复打印日志显示固定文件的问题 "),e("a",ze,[l("107"),o(i)]),l("。")]),Ge])]),Je]),Ve,e("ul",null,[Ke,e("li",null,[l("功能:集成"),Ye,l("库,生成JsonSchema模板 "),e("a",Fe,[l("100"),o(i)]),l(" 。")]),Ze]),$e,e("ul",null,[e("li",null,[l("功能:增加MongoDB 数据库操作 "),e("a",el,[l("93"),o(i)]),l(" 。")]),e("li",null,[l("功能:支持单个用例执行 "),e("a",ll,[l("94"),o(i)]),l(" 。")]),e("li",null,[l("功能:"),ol,l(" 增加"),il,l("参数,发送完邮件删除"),dl,l(" 目录下面的报告和日志文件 "),e("a",tl,[l("95"),o(i)]),l(" 。")]),e("li",null,[l("功能:增加"),sl,l(" 和 "),cl,l(" ,更容易查找json数据 "),e("a",nl,[l("96"),o(i)]),l(" 。")]),rl,ul]),pl,e("ul",null,[al,e("li",null,[l("功能:Http接口测试打印"),_l,l("参数 "),e("a",hl,[l("83"),o(i)])]),gl]),ml,e("ul",null,[fl,bl,e("li",null,[l("接口测试增加打印入参信息 "),e("a",Al,[l("79"),o(i)]),l(" 。")]),kl,Sl,yl,wl]),xl,e("ul",null,[e("li",null,[l("增加 "),vl,l(" 断言方法 "),e("a",Tl,[l("75"),o(i)]),l(" 。")]),e("li",null,[l("修复"),Ql,l("设置"),Pl,l(" 和 "),jl,l("参数,导致用例统计错误 "),e("a",Ml,[l("76"),o(i)]),l(" 。")])]),ql,e("ul",null,[e("li",null,[l("修复HTTP接口测试,指定"),Ll,l("参数错误的问题。"),e("a",Il,[l("71"),o(i)])]),e("li",null,[l("支持发送多人邮件。"),e("a",Hl,[l("72"),o(i)])]),Rl,Cl]),El,e("ul",null,[e("li",null,[l("支持控制台操作步骤显示在HTML报告中。"),e("a",Nl,[l("42"),o(i)])]),e("li",null,[l("修改"),Wl,l("返回空列表。"),e("a",Ol,[l("69"),o(i)])]),e("li",null,[l("修复因为"),Ul,l("/"),Bl,l("导致的编码错误。"),e("a",Xl,[l("70"),o(i)])])]),Dl,e("h3",zl,[Gl,l(" seldom 1.x "),o(t,{type:"tip",text:"v1",vertical:"top"})]),Jl])}const Zl=c(u,[["render",Vl],["__file","CHANGES.html.vue"]]);export{Zl as default}; diff --git a/assets/CHANGES.html-65323456.js b/assets/CHANGES.html-6b115101.js similarity index 83% rename from assets/CHANGES.html-65323456.js rename to assets/CHANGES.html-6b115101.js index caf7322..0ece469 100644 --- a/assets/CHANGES.html-65323456.js +++ b/assets/CHANGES.html-6b115101.js @@ -1 +1 @@ -const e=JSON.parse('{"key":"v-129a7066","path":"/version/CHANGES.html","title":"版本更新","lang":"en-US","frontmatter":{},"headers":[{"level":3,"title":"seldom 3.x","slug":"seldom-3-x","link":"#seldom-3-x","children":[]},{"level":3,"title":"seldom 2.x","slug":"seldom-2-x","link":"#seldom-2-x","children":[]},{"level":3,"title":"seldom 1.x","slug":"seldom-1-x","link":"#seldom-1-x","children":[]},{"level":3,"title":"seldom 0.x","slug":"seldom-0-x","link":"#seldom-0-x","children":[]},{"level":3,"title":"pyse","slug":"pyse","link":"#pyse","children":[]}],"git":{"updatedTime":1731324039000,"contributors":[{"name":"defnngj","email":"defnngj@gmail.com","commits":27},{"name":"fnngj","email":"fnngj@126.com","commits":1}]},"filePathRelative":"version/CHANGES.md"}');export{e as data}; +const e=JSON.parse('{"key":"v-129a7066","path":"/version/CHANGES.html","title":"版本更新","lang":"en-US","frontmatter":{},"headers":[{"level":3,"title":"seldom 3.x","slug":"seldom-3-x","link":"#seldom-3-x","children":[]},{"level":3,"title":"seldom 2.x","slug":"seldom-2-x","link":"#seldom-2-x","children":[]},{"level":3,"title":"seldom 1.x","slug":"seldom-1-x","link":"#seldom-1-x","children":[]},{"level":3,"title":"seldom 0.x","slug":"seldom-0-x","link":"#seldom-0-x","children":[]},{"level":3,"title":"pyse","slug":"pyse","link":"#pyse","children":[]}],"git":{"updatedTime":1734537840000,"contributors":[{"name":"defnngj","email":"defnngj@gmail.com","commits":28},{"name":"fnngj","email":"fnngj@126.com","commits":1}]},"filePathRelative":"version/CHANGES.md"}');export{e as data}; diff --git a/assets/CHANGES.html-df6b9525.js b/assets/CHANGES.html-df6b9525.js deleted file mode 100644 index 9f23f38..0000000 --- a/assets/CHANGES.html-df6b9525.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as c,r as s,o as n,c as r,a as e,b as l,d as o,e as d}from"./app-72107ff1.js";const u={},p=e("h1",{id:"版本更新",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#版本更新","aria-hidden":"true"},"#"),l(" 版本更新")],-1),a={id:"seldom-3-x",tabindex:"-1"},_=e("a",{class:"header-anchor",href:"#seldom-3-x","aria-hidden":"true"},"#",-1),h=e("p",null,[e("strong",null,"3.10.0(2024-11-11)")],-1),g=d("
  • 重要:所有app/web元素定位支持selector模式,详细查看文档。
  • 更新:sleep()增加默认值1s,也支持随机休眠范围:self.seep((1, 3))
  • 更新: appium_lab模块的 Action() 类下面的方法支持自定义休眠时间、间隔时间等。
  • ",3),m=e("code",null,"Steps()",-1),f=e("code",null,"open()",-1),b={href:"https://github.com/SeldomQA/seldom/issues/241",target:"_blank",rel:"noopener noreferrer"},A=e("li",null,[l("告警:"),e("code",null,"type_enter()"),l("添加移除警告,推荐使用"),e("code",null,"type()"),l("。")],-1),k=e("li",null,[l("文档: "),e("ul",null,[e("li",null,"修改playwright使用示例。"),e("li",null,"增加pyAutoGUI使用示例。")])],-1),S=e("p",null,[e("strong",null,"3.9.1(2024-10-10)")],-1),y=d("
  • 更新:脚手架项目模板,增加run.py文件。
  • 修复:生成随机数,获取在线时间接口错误。
  • 修复:datetime.utcnow()在Python 3.12 告警。
  • App测试:
  • API测试:
  • ",5),w=e("code",null,"prompt_value()",-1),x={href:"https://github.com/SeldomQA/seldom/issues/166",target:"_blank",rel:"noopener noreferrer"},v=e("code",null,"action_chains()",-1),T=e("code",null,"ActionChains()",-1),Q={href:"https://github.com/SeldomQA/seldom/issues/119",target:"_blank",rel:"noopener noreferrer"},P=e("code",null,"is_visible()",-1),j={href:"https://github.com/SeldomQA/seldom/issues/62",target:"_blank",rel:"noopener noreferrer"},M=e("li",null,[e("code",null,"Pycharm"),l("右键运行Web UI用例,抛异常提示。")],-1),q={href:"https://github.com/SeldomQA/seldom/issues/31",target:"_blank",rel:"noopener noreferrer"},L={href:"https://github.com/SeldomQA/seldom/issues/174",target:"_blank",rel:"noopener noreferrer"},I=e("li",null,[l("升级:"),e("code",null,"XTestRunner==1.8.0"),l("。")],-1),H=d("

    3.9.0(2024-09-09)

    3.8.1(2024-08-20)

    3.8.0(2024-07-06)

    3.7.1(2024-06-01)

    3.7.0(2024-05-06)

    3.6.0(2024-03-04)

    3.5.0(2024-01-14)

    3.4.1(2023-11-26)

    ",15),R=e("li",null,[l("修复:"),e("code",null,"diff_json()"),l(" 对比特殊数据的异常没有捕捉到。")],-1),C=e("li",null,[e("code",null,"setUpClass()"),l("/"),e("code",null,"tearDownClass()"),l(" 增加异常捕捉,避免报错之后,用例无法统计的问题。")],-1),E=e("code",null,"screenshots()",-1),N={href:"https://github.com/SeldomQA/seldom/issues/201",target:"_blank",rel:"noopener noreferrer"},W=e("li",null,[e("code",null,"open_electron()"),l(" 增加chromedriver_path参数,支持手动指定驱动地址。")],-1),O=e("p",null,[e("strong",null,"3.4.0(2023-11-18)")],-1),U=d("
  • 新增:dependent_func()装饰器,支持用例方法依赖调用,具体使用参考文档。
  • api测试
  • web测试
  • cache操作日志增加 emoji。
  • ",4),B=e("code",null,"diff_json()",-1),X={href:"https://github.com/SeldomQA/seldom/issues/197",target:"_blank",rel:"noopener noreferrer"},D=e("p",null,[e("strong",null,"3.3.0(2023-09-26)")],-1),z=d("
  • web测试
  • 随机数据
  • 增加运行时内嵌(built-in)方法:base_url()driver() - 无需导入,可以在自动化程序任意位置使用这两个方法。
  • 移除parameterized 库的依赖,改为内置。
  • ",4),G=e("code",null,"diff_json()",-1),J=e("code",null,"[{}]",-1),V={href:"https://github.com/SeldomQA/seldom/issues/197",target:"_blank",rel:"noopener noreferrer"},K=e("p",null,[e("strong",null,"3.2.3(2023-07-30)")],-1),Y=d("
  • HTTP自动化
  • Web自动化
  • App测试
  • 优化:seldom_log.log 文件只记录一次运行结果,减少文件大小。
  • ",4),F=e("code",null,"webdriver_manager==4.0.0",-1),Z={href:"https://github.com/SeldomQA/seldom/issues/189",target:"_blank",rel:"noopener noreferrer"},$=e("li",null,[l("其他: 添加 "),e("code",null,"pyproject.toml"),l(" 支持。")],-1),ee=e("li",null,"文档:增加其他库的使用例子。",-1),le=d("

    3.2.2(2023-05-10)

    3.2.1(2023-04-14)

    3.2.0(2023-03-14)

    3.1.3(2023-02-15)

    ",7),oe=e("code",null,"file_data()",-1),ie=e("code",null,"end_line",-1),de={href:"https://github.com/SeldomQA/seldom/issues/163",target:"_blank",rel:"noopener noreferrer"},te=e("li",null,[l("优化:"),e("code",null,"self.assertElement()"),l(" 断言元素时间过长的问题。")],-1),se=e("li",null,[l("优化:"),e("code",null,"self.assertJSON()"),l(" 断言日志,区分告警和错误。")],-1),ce=e("li",null,[l("移除:"),e("code",null,"self.jresponse()"),l(" 方法。")],-1),ne=d("

    3.1.2(internal)

    内部版本:移除了日志打印的 emoji 表情。

    3.1.1(2023-01-03)

    3.1.0(2022-12-15)

    ",6),re=e("li",null,[l("功能:提供 "),e("code",null,"confrun.py"),l(" 运行配置文件,配合 "),e("code",null,"seldom"),l(" 命令使用。")],-1),ue=e("li",null,[l("功能:Web测试,增加 "),e("code",null,"self.get_log()"),l(" 方法。")],-1),pe=e("code",null,"webdriver_manager==3.8.5",-1),ae={href:"https://github.com/SeldomQA/seldom/issues/159",target:"_blank",rel:"noopener noreferrer"},_e={href:"https://github.com/SeldomQA/seldom/issues/158",target:"_blank",rel:"noopener noreferrer"},he=e("li",null,[l("修复:Web测试, "),e("code",null,"self.close()"),l(" 关闭浏览器Bug。")],-1),ge=d("

    3.0.1(2022-11-5)

    3.0.0(2022-10-31)

    3.0.0beta2(2022-10-26)

    ",5),me=e("code",null,"r.text",-1),fe={href:"https://github.com/SeldomQA/seldom/issues/146",target:"_blank",rel:"noopener noreferrer"},be=e("li",null,[l("app测试:感谢 @986379041 "),e("ul",null,[e("li",null,[e("code",null,"install_app()"),l(" 错误")]),e("li",null,[e("code",null,"close_app()"),l(" 错误")])])],-1),Ae=e("code",null,"TestMainExtend",-1),ke=e("code",null,"tester",-1),Se={href:"https://github.com/SeldomQA/seldom/issues/149",target:"_blank",rel:"noopener noreferrer"},ye=e("code",null,"get_month()",-1),we=e("code",null,"get_year()",-1),xe={href:"https://github.com/SeldomQA/seldom/issues/152",target:"_blank",rel:"noopener noreferrer"},ve=e("code",null,"> seldom --clear-cache true",-1),Te={href:"https://github.com/SeldomQA/seldom/issues/153",target:"_blank",rel:"noopener noreferrer"},Qe=e("li",null,[l("其他: "),e("ul",null,[e("li",null,"seldom 运行用例,优化内存使用。")])],-1),Pe=d("

    3.0.0beta1(2022-10-03)

    ",2),je={id:"seldom-2-x",tabindex:"-1"},Me=e("a",{class:"header-anchor",href:"#seldom-2-x","aria-hidden":"true"},"#",-1),qe=d("

    2.10.6/7(2022-09-07)

    2.10.4/5(2022-08-17)

    2.10.3(2022-07-17)

    2.10.2(2022-06-25)

    ",8),Le={href:"https://github.com/SeldomQA/seldom/blob/master/docs/vpdocs/other/other.md",target:"_blank",rel:"noopener noreferrer"},Ie=e("li",null,"功能:测试报告显示断言信息。",-1),He=e("li",null,[l("功能:"),e("code",null,"main()"),l(" 通过"),e("code",null,"open=False"),l("可以控制运行完测试 不自动化打开测试报告。")],-1),Re=e("li",null,[l("增加"),e("code",null,"self.new_browser()"),l(" 可以打开新的浏览器,但只能使用"),e("code",null,"selenium"),l(" 的 API")],-1),Ce=e("code",null,"switch_to_frame_parent",-1),Ee={href:"https://github.com/SeldomQA/seldom/issues/118",target:"_blank",rel:"noopener noreferrer"},Ne=e("code",null,"assertNotElement",-1),We={href:"https://github.com/SeldomQA/seldom/issues/120",target:"_blank",rel:"noopener noreferrer"},Oe=e("li",null,[l("HTTP 测试: "),e("ul",null,[e("li",null,"优化:JSON日志进行格式化打印。")])],-1),Ue=e("p",null,[e("strong",null,"2.10.1(2022-05-30)")],-1),Be=e("ul",null,[e("li",null,"修复:seldom log 问题引起,错误信息无法在控制台打印。")],-1),Xe={href:"https://github.com/SeldomQA/seldom/issues/107",target:"_blank",rel:"noopener noreferrer"},De=e("p",null,[e("strong",null,"2.10.0(2022-05-25)")],-1),ze={href:"https://github.com/SeldomQA/seldom/issues/107",target:"_blank",rel:"noopener noreferrer"},Ge=e("li",null,[l("log方法变更:"),e("code",null,"log.warn()"),l(" -> "),e("code",null,"log.warning()"),l("。")],-1),Je=d("
  • 功能:提供了cache 类来模拟缓存。
  • 功能:@data() 装饰器支持 dict 格式。
  • 功能:self.jresponse() 方法设计不合理,给以废弃提示;可以使用self.jsonpath()/self.jmespath() 替代。
  • 优化:断言方法assertSchema()assertJSON()支持response传参。
  • 优化:@check_response() check检查失败打印response
  • 修复:webdriver_manager 没有设置上限版本,导致webdriver_manager>=3.6.x 报错; 如果使用的 seldom<=2.9 请重新安装webdriver_manager==3.5.2
  • ",6),Ve=e("p",null,[e("strong",null,"2.9.0(2022-04-30)")],-1),Ke=d("
  • seldom log功能:
  • 功能:提供了@check_response() 装饰器,为接口封装提供强大的支持。
  • ",2),Ye=e("code",null,"genson",-1),Fe={href:"https://github.com/SeldomQA/seldom/issues/100",target:"_blank",rel:"noopener noreferrer"},Ze=d("
  • 功能:增加assertInPath() 断言方法。
  • 功能:增加jmespath()方法,方便提取测试数据。
  • 优化:jresponse() 增加对jmespath 语法的支持。
  • 优化:支持self.get()/self.post()/self.put()/self.delete() 返回response对象。
  • ",4),$e=e("p",null,[e("strong",null,"2.8.0(2022-04-16)")],-1),el={href:"https://github.com/SeldomQA/seldom/issues/93",target:"_blank",rel:"noopener noreferrer"},ll={href:"https://github.com/SeldomQA/seldom/issues/94",target:"_blank",rel:"noopener noreferrer"},ol=e("code",null,"sendmail()",-1),il=e("code",null,"delete",-1),dl=e("code",null,"reports/",-1),tl={href:"https://github.com/SeldomQA/seldom/issues/95",target:"_blank",rel:"noopener noreferrer"},sl=e("code",null,"jsonpath",-1),cl=e("code",null,"jresponse()",-1),nl={href:"https://github.com/SeldomQA/seldom/issues/96",target:"_blank",rel:"noopener noreferrer"},rl=e("li",null,[l("功能:创建项目脚手架增加api测试例子:"),e("code",null,"seldom -project mypro"),l(" 。")],-1),ul=e("li",null,"其他: 全新的seldom在线文档:https://seldomqa.github.io/ ,感谢 @nickliya",-1),pl=d("

    2.7.0(2022-03-26)

    2.6.0(2022-03-18)

    2.5.1(2022-02-19)

    ",5),al=e("li",null,"功能:Http接口测试使用日志打印接口信息",-1),_l=e("code",null,"json",-1),hl={href:"https://github.com/SeldomQA/seldom/issues/83",target:"_blank",rel:"noopener noreferrer"},gl=e("li",null,[l("修复:Web UI测试"),e("code",null,"self.Key()"),l(" 无法定位元素的问题")],-1),ml=d("

    2.5.0(2022-01-30)

    2.4.2(2022-01-18)

    2.4.1(2022-01-17)

    2.4.0(2022-01-02)

    ",7),fl=e("li",null,"适配selenium 4.0+ ,适配相关依赖库新版本。",-1),bl=e("li",null,[l("测试用例支持"),e("code",null,"label"),l("标签分类。")],-1),Al={href:"https://github.com/SeldomQA/seldom/issues/79",target:"_blank",rel:"noopener noreferrer"},kl=e("li",null,[l("EdgeChromium浏览器支持"),e("code",null,"headless"),l("模式。")],-1),Sl=e("li",null,[l("Web自动化测试增加元素截图"),e("code",null,"self.element_screenshot()")],-1),yl=e("li",null,"优化HTML测试报告样式。",-1),wl=e("li",null,"优化邮件模板样式。",-1),xl=e("p",null,[e("strong",null,"2.3.3(2021-11-12)")],-1),vl=e("code",null,"assertNotText()",-1),Tl={href:"https://github.com/SeldomQA/seldom/issues/75",target:"_blank",rel:"noopener noreferrer"},Ql=e("code",null,"main()",-1),Pl=e("code",null,"rerun",-1),jl=e("code",null,"save_last_run",-1),Ml={href:"https://github.com/SeldomQA/seldom/issues/76",target:"_blank",rel:"noopener noreferrer"},ql=d("

    2.3.2(2021-11-08)

    2.3.1(2021-11-02)

    2.3.0(2021-10-18)

    2.2.4(2021-09-21)

    ",7),Ll=e("code",null,"url",-1),Il={href:"https://github.com/SeldomQA/seldom/issues/71",target:"_blank",rel:"noopener noreferrer"},Hl={href:"https://github.com/SeldomQA/seldom/issues/72",target:"_blank",rel:"noopener noreferrer"},Rl=e("li",null,"优化HTMLTestRunner, 重跑次数不记录为用例数。",-1),Cl=e("li",null,[l("修复pip安装缺少"),e("code",null,"description.rst"),l(" 问题。")],-1),El=e("p",null,[e("strong",null,"2.2.3(2021-08-27)")],-1),Nl={href:"https://github.com/SeldomQA/seldom/issues/42",target:"_blank",rel:"noopener noreferrer"},Wl=e("code",null,"get_elements()",-1),Ol={href:"https://github.com/SeldomQA/seldom/issues/69",target:"_blank",rel:"noopener noreferrer"},Ul=e("code",null,"colorama",-1),Bl=e("code",null,"emoji",-1),Xl={href:"https://github.com/SeldomQA/seldom/issues/70",target:"_blank",rel:"noopener noreferrer"},Dl=d("

    2.2.2(2021-08-13)

    2.2.1(2021-06-30)

    2.2.0(2021-06-15)

    2.1.1(2021-05-28)

    2.1.0(2021-05-19)

    2.0.1(2021-05-07)

    2.0.0(2021-04-24)

    2.0.0.beta(2021-03-24)

    ",16),zl={id:"seldom-1-x",tabindex:"-1"},Gl=e("a",{class:"header-anchor",href:"#seldom-1-x","aria-hidden":"true"},"#",-1),Jl=d('

    1.10.3(2021-03-23)

    ...

    1.10.2(2021-03-13)

    1.10.1(2021-03-04)

    1.10.0(2021-01-29)

    1.9.0(2020-12-19)

    1.8.0(2020-11-17)

    1.7.2(2020-10-10)

    1.7.0(2020-09-21)

    1.6.0(2020-08-24)

    1.5.6(2020-07-24)

    1.5.5(2020-06-29)

    1.5.4(2020-06-04)

    1.5.3(2020-05-31)

    1.5.2(202x-05-16)

    1.5.1 (2020-05-14)

    1.5.0(2020-04-29)

    1.2.6 (2020-04-22)

    1.2.5(2020-04-13)

    1.2.4(2020-03-19)

    1.2.3(2020-03-11)

    1.2.2(2020-03-03)

    1.2.0(2020-02-01)

    Global launch browser

    1.1.0(2020-01-19)

    selenium grid support Added safari support

    1.0.0(2020-01-04)

    The framework function has been basically improved. I'm glad to release version 1.0

    seldom 0.x

    0.3.6(2019-12-23)

    Add cookie manipulation APIs Optimized element wait

    0.3.5(2019-12-06)

    Added chrome/firefox browser driver download command Driver file path Settings are supported

    0.3.3(2019-11-30)

    add skip case

    0.3.2(2019-11-27)

    Added a switch to display the last rerun result Optimized assertion method

    0.3.0(2019-11-21)

    Update element positioning

    0.2.0(2019-11-17)

    Change the project name to seldom Introducing the poium test library,

    pyse

    0.1.5(2019-11-15)

    0.0.9(2018-03-29)

    Simplifying API calls

    0.0.8(2017-11-23)

    add parameterized Beautification test report

    0.0.7(2016-11-09)

    Re based on unittest.

    0.0.6(2016-04-29)

    add setup.py file, Specification of the installation process, a time to install all dependencies. Delete unnecessary files

    0.0.5

    Increase the support of multiple positioning methods

    0.0.4

    Method to add default to wait. Modify the realization of the individual methods

    0.0.3.1 version update

    0.0.3 version update(2015-09-08)

    0.0.2 version update(2015-09-08)

    ',82);function Vl(Kl,Yl){const t=s("Badge"),i=s("ExternalLinkIcon");return n(),r("div",null,[p,e("h3",a,[_,l(" seldom 3.x "),o(t,{type:"tip",text:"v3",vertical:"top"})]),h,e("ul",null,[g,e("li",null,[l("修复:"),m,l("类的 "),f,l(" 方法默认传url报错 "),e("a",b,[l("#241"),o(i)]),l("。")]),A,k]),S,e("ul",null,[y,e("li",null,[l("Web测试: "),e("ul",null,[e("li",null,[l("增加"),w,l("方法,支持弹窗输入 "),e("a",x,[l("#166"),o(i)]),l("。")]),e("li",null,[l("增加"),v,l("方法,返回Selenium的"),T,l(" 类对象 "),e("a",Q,[l("#119"),o(i)]),l("。")]),e("li",null,[l("增加"),P,l("方法,检查页面元素是否可见 "),e("a",j,[l("#62"),o(i)]),l("。")]),M])]),e("li",null,[l("文档更新: "),e("ul",null,[e("li",null,[l("增加浏览器代理设置示例 "),e("a",q,[l("#31"),o(i)]),l("。")]),e("li",null,[l("操作已打开浏览器示例 "),e("a",L,[l("#174"),o(i)]),l("。")])])]),I]),H,e("ul",null,[R,C,e("li",null,[l("web测试 "),e("ul",null,[e("li",null,[E,l(" 增加images参数,支持传入截图对象 "),e("a",N,[l("#202"),o(i)]),l("。")]),W])])]),O,e("ul",null,[U,e("li",null,[l("修复:"),B,l(" 优化,支持dict深度排序。 "),e("a",X,[l("#197"),o(i)])])]),D,e("ul",null,[z,e("li",null,[l("修复:"),G,l(" 对比 "),J,l(" 数据时报错。 "),e("a",V,[l("#197"),o(i)])])]),K,e("ul",null,[Y,e("li",null,[l("升级:"),F,l(),e("a",Z,[l("#189"),o(i)])]),$,ee]),le,e("ul",null,[e("li",null,[l("功能:"),oe,l(" 增加"),ie,l(" 参数,对于csv/excel文件支持读取到第几行结束。"),e("a",de,[l("#163"),o(i)])]),te,se,ce]),ne,e("ul",null,[re,ue,e("li",null,[l("升级:"),pe,l(" ,支持Mac M1芯片的浏览器驱动。"),e("a",ae,[l("#159"),o(i)])]),e("li",null,[l("修复:seldom-platform平台同步多个项目引起的Bug。"),e("a",_e,[l("#158"),o(i)])]),he]),ge,e("ul",null,[e("li",null,[l("修复: "),e("ul",null,[e("li",null,[l("接口测试: 接口返回文本"),me,l(" 中文乱码问题。"),e("a",fe,[l("#146"),o(i)])]),be])]),e("li",null,[l("功能: "),e("ul",null,[e("li",null,[Ae,l(" 类增加 "),ke,l("参数。 "),e("a",Se,[l("#149"),o(i)])]),e("li",null,[l("生成随机数,增加"),ye,l(" 和 "),we,l("方法。 "),e("a",xe,[l("#152"),o(i)])]),e("li",null,[l("seldom命令增加清除所有缓存。"),ve,l("。 "),e("a",Te,[l("#153"),o(i)])])])]),Qe]),Pe,e("h3",je,[Me,l(" seldom 2.x "),o(t,{type:"tip",text:"v2",vertical:"top"})]),qe,e("p",null,[l("更新:移动模式列表更新,去掉旧设备,增加新设备 "),e("a",Le,[l("link"),o(i)])]),e("ul",null,[Ie,He,e("li",null,[l("Web 测试: "),e("ul",null,[Re,e("li",null,[l("增加"),Ce,l(" 切换到上一级表单,"),e("a",Ee,[l("#118"),o(i)]),l("。")]),e("li",null,[l("优化"),Ne,l(" 执行慢的情况 "),e("a",We,[l("#120"),o(i)])])])]),Oe]),Ue,Be,e("blockquote",null,[e("p",null,[l("2.10.0 为了解决"),e("a",Xe,[l("107"),o(i)]),l(" 问题,我们经过反复的讨论和优化,甚至对相关库XTestRunner做了修改;以为完美解决了这个问题,没想到还是引起了一些严重的错误。为此,我们感到非常沮丧,退回到2.9.0的实现方案。请升级到2.10.1版本。")])]),De,e("ul",null,[e("li",null,[l("seldom log功能: "),e("ul",null,[e("li",null,[l("修复打印日志显示固定文件的问题 "),e("a",ze,[l("107"),o(i)]),l("。")]),Ge])]),Je]),Ve,e("ul",null,[Ke,e("li",null,[l("功能:集成"),Ye,l("库,生成JsonSchema模板 "),e("a",Fe,[l("100"),o(i)]),l(" 。")]),Ze]),$e,e("ul",null,[e("li",null,[l("功能:增加MongoDB 数据库操作 "),e("a",el,[l("93"),o(i)]),l(" 。")]),e("li",null,[l("功能:支持单个用例执行 "),e("a",ll,[l("94"),o(i)]),l(" 。")]),e("li",null,[l("功能:"),ol,l(" 增加"),il,l("参数,发送完邮件删除"),dl,l(" 目录下面的报告和日志文件 "),e("a",tl,[l("95"),o(i)]),l(" 。")]),e("li",null,[l("功能:增加"),sl,l(" 和 "),cl,l(" ,更容易查找json数据 "),e("a",nl,[l("96"),o(i)]),l(" 。")]),rl,ul]),pl,e("ul",null,[al,e("li",null,[l("功能:Http接口测试打印"),_l,l("参数 "),e("a",hl,[l("83"),o(i)])]),gl]),ml,e("ul",null,[fl,bl,e("li",null,[l("接口测试增加打印入参信息 "),e("a",Al,[l("79"),o(i)]),l(" 。")]),kl,Sl,yl,wl]),xl,e("ul",null,[e("li",null,[l("增加 "),vl,l(" 断言方法 "),e("a",Tl,[l("75"),o(i)]),l(" 。")]),e("li",null,[l("修复"),Ql,l("设置"),Pl,l(" 和 "),jl,l("参数,导致用例统计错误 "),e("a",Ml,[l("76"),o(i)]),l(" 。")])]),ql,e("ul",null,[e("li",null,[l("修复HTTP接口测试,指定"),Ll,l("参数错误的问题。"),e("a",Il,[l("71"),o(i)])]),e("li",null,[l("支持发送多人邮件。"),e("a",Hl,[l("72"),o(i)])]),Rl,Cl]),El,e("ul",null,[e("li",null,[l("支持控制台操作步骤显示在HTML报告中。"),e("a",Nl,[l("42"),o(i)])]),e("li",null,[l("修改"),Wl,l("返回空列表。"),e("a",Ol,[l("69"),o(i)])]),e("li",null,[l("修复因为"),Ul,l("/"),Bl,l("导致的编码错误。"),e("a",Xl,[l("70"),o(i)])])]),Dl,e("h3",zl,[Gl,l(" seldom 1.x "),o(t,{type:"tip",text:"v1",vertical:"top"})]),Jl])}const Zl=c(u,[["render",Vl],["__file","CHANGES.html.vue"]]);export{Zl as default}; diff --git a/assets/advanced.html-c039b7bf.js b/assets/advanced.html-75e93128.js similarity index 90% rename from assets/advanced.html-c039b7bf.js rename to assets/advanced.html-75e93128.js index b37c6fe..61ec6b9 100644 --- a/assets/advanced.html-c039b7bf.js +++ b/assets/advanced.html-75e93128.js @@ -1 +1 @@ -const e=JSON.parse('{"key":"v-d8f79a72","path":"/getting-started/advanced.html","title":"高级用法","lang":"en-US","frontmatter":{},"headers":[{"level":3,"title":"fixture","slug":"fixture","link":"#fixture","children":[]},{"level":3,"title":"跳过测试","slug":"跳过测试","link":"#跳过测试","children":[]},{"level":3,"title":"重复执行","slug":"重复执行","link":"#重复执行","children":[]},{"level":3,"title":"随机测试数据","slug":"随机测试数据","link":"#随机测试数据","children":[]},{"level":3,"title":"用例的依赖","slug":"用例的依赖","link":"#用例的依赖","children":[]},{"level":3,"title":"用例分类标签","slug":"用例分类标签","link":"#用例分类标签","children":[]},{"level":3,"title":"发送邮件","slug":"发送邮件","link":"#发送邮件","children":[]},{"level":3,"title":"发送钉钉","slug":"发送钉钉","link":"#发送钉钉","children":[]},{"level":3,"title":"seldom日志","slug":"seldom日志","link":"#seldom日志","children":[]},{"level":3,"title":"缓存 cache","slug":"缓存-cache","link":"#缓存-cache","children":[]}],"git":{"updatedTime":1725890482000,"contributors":[{"name":"defnngj","email":"defnngj@gmail.com","commits":14},{"name":"Yongchin","email":"yongchin39@qq.com","commits":1},{"name":"xiaosedl","email":"deng@mail.com","commits":1}]},"filePathRelative":"getting-started/advanced.md"}');export{e as data}; +const e=JSON.parse('{"key":"v-d8f79a72","path":"/getting-started/advanced.html","title":"高级用法","lang":"en-US","frontmatter":{},"headers":[{"level":3,"title":"fixture","slug":"fixture","link":"#fixture","children":[]},{"level":3,"title":"跳过测试","slug":"跳过测试","link":"#跳过测试","children":[]},{"level":3,"title":"重复执行","slug":"重复执行","link":"#重复执行","children":[]},{"level":3,"title":"随机测试数据","slug":"随机测试数据","link":"#随机测试数据","children":[]},{"level":3,"title":"用例的依赖","slug":"用例的依赖","link":"#用例的依赖","children":[]},{"level":3,"title":"用例分类标签","slug":"用例分类标签","link":"#用例分类标签","children":[]},{"level":3,"title":"发送邮件","slug":"发送邮件","link":"#发送邮件","children":[]},{"level":3,"title":"发送钉钉","slug":"发送钉钉","link":"#发送钉钉","children":[]},{"level":3,"title":"seldom日志","slug":"seldom日志","link":"#seldom日志","children":[]},{"level":3,"title":"缓存 cache","slug":"缓存-cache","link":"#缓存-cache","children":[]}],"git":{"updatedTime":1733479322000,"contributors":[{"name":"defnngj","email":"defnngj@gmail.com","commits":16},{"name":"Yongchin","email":"yongchin39@qq.com","commits":1},{"name":"xiaosedl","email":"deng@mail.com","commits":1}]},"filePathRelative":"getting-started/advanced.md"}');export{e as data}; diff --git a/assets/advanced.html-c2d9b916.js b/assets/advanced.html-ee1e4a3d.js similarity index 89% rename from assets/advanced.html-c2d9b916.js rename to assets/advanced.html-ee1e4a3d.js index 1857e48..0f4bd72 100644 --- a/assets/advanced.html-c2d9b916.js +++ b/assets/advanced.html-ee1e4a3d.js @@ -1,4 +1,4 @@ -import{_ as n,o as s,c as a,e as t}from"./app-72107ff1.js";const e={},p=t(`

    高级用法

    fixture

    有时自动化测试用例的运行需要一些前置&后置步骤,seldom提供了相应的方法。

    seldom重写了unittest的fixture,所以,请使用seldom的fixture,对应表格。

    unittestseldom说明
    setUpClass(cls)start_class(cls)测试类开始执行。
    tearDownClass(cls)end_class(cls)测试类结束执行。
    setUp(self)start(self)测试方法(用例)开始执行。
    tearDown(self)end(self)测试方法(用例)结束执行。

    示例

    针对每条测试类/测试用例的fixture使用示例。

    # test_fixture.py
    +import{_ as n,o as s,c as a,e as t}from"./app-9fb6f1b5.js";const e={},p=t(`

    高级用法

    fixture

    有时自动化测试用例的运行需要一些前置&后置步骤,seldom提供了相应的方法。

    seldom重写了unittest的fixture,所以,请使用seldom的fixture,对应表格。

    unittestseldom说明
    setUpClass(cls)start_class(cls)测试类开始执行。
    tearDownClass(cls)end_class(cls)测试类结束执行。
    setUp(self)start(self)测试方法(用例)开始执行。
    tearDown(self)end(self)测试方法(用例)结束执行。
    -start_run()confrun.py文件配置,整个用例开始前运行。
    -end_run()confrun.py文件配置,整个用例结束后运行。

    示例1

    针对每条测试类/测试用例的fixture使用示例。

    # test_fixture.py
     import seldom
     
     
    @@ -38,7 +38,51 @@ test_case_two (zzz_case.TestCase.test_cas
     ok
     测试类结束执行
     ...
    -

    跳过测试

    seldom 提供了跳过用例的装饰用于跳过暂时不执行的用例。

    装饰器

    • seldom.skip():无条件地跳过一个测试。
    • seldom.skip_if(): 如果条件为真,则跳过测试。
    • seldom.skip_unless(): 跳过一个测试,除非条件为真。
    • seldom.expected_failure(): 预期测试用例会失败。
    • self.skipTest(): 根据条件跳过测试。

    使用方法

    # test_skip.py
    +

    示例2

    有时候我们需要整个测试开始前结束后完成一些工作,可以通过下面的方式配置。

    • 目录结构
    mypro/
    +├── test_dir/
    +│   ├── __init__.py
    +│   ├── test_sample.py
    +├── confrun.py
    +└── run.py
    +
    • confrun.py 配置前后置动作
    from seldom.logging import log
    +from seldom.utils import cache
    +
    +
    +def start_run():
    +    """
    +    Test the hook function before running
    +    """
    +    log.info("start_run")
    +    cache.set({"token": "token123"})
    +
    +
    +def end_run():
    +    """
    +    Test the hook function after running
    +    """
    +    log.info("end_run")
    +    cache.clear("token")
    +

    示例中用于添加和清除 cache, 根据实际需求你可以加上任何动作。

    • run.py 执行用例
    import seldom
    +
    +if __name__ == '__main__':
    +    seldom.main(path="./test_dir")
    +
    • 运行结果
    > python run.py
    +...
    +
    +2024-12-06 17:55:04 | INFO     | confrun.py | MainThread | start_run   # confrun.py 所有用例前的动作
    +2024-12-06 17:55:04 | INFO     | cache.py | MainThread | 💾 Set cache data: token = token123
    +
    +2024-12-06 17:55:04 | INFO     | runner.py | MainThread | TestLoader: ./test_dir
    +XTestRunner Running tests...
    +----------------------------------------------------------------------
    +2024-12-06 17:55:04 | INFO     | cache.py | MainThread | 💾 Get cache data: token = token123
    +Generating HTML reports...
    +.12024-12-06 17:55:04 | SUCCESS  | runner.py | MainThread | generated html file: file:///D:\\github\\seldomQA\\seldom\\reports\\2024_12_06_17_55_03_result.html
    +2024-12-06 17:55:04 | SUCCESS  | runner.py | MainThread | generated log file: file:///D:\\github\\seldomQA\\seldom\\reports\\seldom_log.log
    +
    +2024-12-06 17:55:04 | INFO     | confrun.py | MainThread | end_run  # confrun.py 所有用例后的动作
    +2024-12-06 17:55:04 | INFO     | cache.py | MainThread | 💾 Clear cache data: token
    +

    跳过测试

    seldom 提供了跳过用例的装饰用于跳过暂时不执行的用例。

    装饰器

    • seldom.skip():无条件地跳过一个测试。
    • seldom.skip_if(): 如果条件为真,则跳过测试。
    • seldom.skip_unless(): 跳过一个测试,除非条件为真。
    • seldom.expected_failure(): 预期测试用例会失败。
    • self.skipTest(): 根据条件跳过测试。

    使用方法

    # test_skip.py
     import seldom
     
     
    @@ -459,4 +503,4 @@ cache.clear# 清除 \`add()\` 函数缓存
         dc.clear("add")
         seldom.main(debug=True)
    -
    `,96),o=[p];function c(l,i){return s(),a("div",null,o)}const d=n(e,[["render",c],["__file","advanced.html.vue"]]);export{d as default}; +
    `,107),o=[p];function c(l,i){return s(),a("div",null,o)}const d=n(e,[["render",c],["__file","advanced.html.vue"]]);export{d as default}; diff --git a/assets/advanced.html-ef34530d.js b/assets/advanced.html-ef34530d.js deleted file mode 100644 index aba2cf1..0000000 --- a/assets/advanced.html-ef34530d.js +++ /dev/null @@ -1,462 +0,0 @@ -import{_ as n,o as s,c as a,e as t}from"./app-a06a2d51.js";const e={},p=t(`

    高级用法

    fixture

    有时自动化测试用例的运行需要一些前置&后置步骤,seldom提供了相应的方法。

    seldom重写了unittest的fixture,所以,请使用seldom的fixture,对应表格。

    unittestseldom说明
    setUpClass(cls)start_class(cls)测试类开始执行。
    tearDownClass(cls)end_class(cls)测试类结束执行。
    setUp(self)start(self)测试方法(用例)开始执行。
    tearDown(self)end(self)测试方法(用例)结束执行。

    示例

    针对每条测试类/测试用例的fixture使用示例。

    # test_fixture.py
    -import seldom
    -
    -
    -class TestCase(seldom.TestCase):
    -
    -    @classmethod
    -    def start_class(cls):
    -        print("测试类开始执行")
    -
    -    @classmethod
    -    def end_class(cls):
    -        print("测试类结束执行")
    -
    -    def start(self):
    -        print("一条测试用例开始")
    -
    -    def end(self):
    -        print("一条测试结果")
    -
    -    def test_case_one(self):
    -        ...
    -
    -    def test_case_two(self):
    -        ...
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(debug=True)
    -

    警告:不要把用例的操作步骤写到start_class/end_class中! 因为它不属于某条用例的一部分,一旦里面的操作步骤运行失败,会影响用例的执行。

    运行结果

    > python test_fixture.py
    -...
    -测试类开始执行
    -test_case_one (zzz_case.TestCase.test_case_one) ... 一条测试用例开始
    -一条测试结果
    -ok
    -test_case_two (zzz_case.TestCase.test_case_two) ... 一条测试用例开始
    -一条测试结果
    -ok
    -测试类结束执行
    -...
    -

    跳过测试

    seldom 提供了跳过用例的装饰用于跳过暂时不执行的用例。

    装饰器

    使用方法

    # test_skip.py
    -import seldom
    -
    -
    -@seldom.skip(reason="跳过类")
    -class SkipTest(seldom.TestCase):
    -
    -    def test_case(self):
    -        ...
    -
    -
    -class YouTest(seldom.TestCase):
    -
    -    @seldom.skip(reason="跳过用例")
    -    def test_skip_case(self):
    -        ...
    -
    -    def test_if_skip(self):
    -        login = False
    -        if login is False:
    -            self.skipTest(reason="登录失败,跳过后续执行")
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(debug=True)
    -
    -

    重复执行

    当然某一段测试需要重复执行,使用for循环是常规的操作,seldom提供了rerun() 方法可以更优雅的完成这个工作。

    import seldom
    -from seldom import rerun
    -
    -
    -class TestCase(seldom.TestCase):
    -
    -    @rerun(100)
    -    def test_search_seldom(self):
    -        self.open("https://www.baidu.com")
    -        self.type_enter(id_="kw", text="seldom")
    -
    -

    通过@rerun() 装饰 test_searchseldom() 可以执行 100 次,统计结果仍为1条用例,如果想统计为 100 条用例,请使用@data() 装饰器。

    随机测试数据

    测试数据是测试用例的重要部分,有时不能把测试数据写死在测试用例中,比如注册新用户,一旦执行过用例那么测试数据就已经存在了,所以每次执行注册新用户的数据不能是一样的,这就需要随机生成一些测试数据。

    seldom 提供了随机获取测试数据的方法。

    import seldom
    -from seldom import testdata
    -
    -
    -class YouTest(seldom.TestCase):
    -
    -    def test_case(self):
    -        """a simple test case """
    -        word = testdata.get_word()
    -        print(word)
    -
    -
    -if __name__ == '__main__':
    -    seldom.main()
    -

    通过get_word() 随机获取一个单词,然后对这个单词进行搜索。

    更多的方法

    from seldom.testdata import *
    -
    -# 随机一个名字
    -print("名字:", first_name())
    -print("名字(男):", first_name(gender="male"))
    -print("名字(女):", first_name(gender="female"))
    -print("名字(中文男):", first_name(gender="male", language="zh"))
    -print("名字(中文女):", first_name(gender="female", language="zh"))
    -
    -# 随机一个姓
    -print("姓:", last_name())
    -print("姓(中文):", last_name(language="zh"))
    -
    -# 随机一个姓名
    -print("姓名:", username())
    -print("姓名(中文):", username(language="zh"))
    -
    -# 随机一个生日
    -print("生日:", get_birthday())
    -print("生日字符串:", get_birthday(as_str=True))
    -print("生日年龄范围:", get_birthday(start_age=20, stop_age=30))
    -
    -# 日期
    -print("日期(当前):", get_date())
    -print("日期(昨天):", get_date(-1))
    -print("日期(明天):", get_date(1))
    -
    -print("当月:", get_month())
    -print("上个月:", get_month(-1))
    -print("下个月:", get_month(1))
    -
    -print("今年:", get_year())
    -print("去年:", get_year(-1))
    -print("明年:", get_year(1))
    -
    -# 数字
    -print("数字(8位):", get_digits(8))
    -
    -# 邮箱
    -print("邮箱:", get_email())
    -
    -# 浮点数
    -print("浮点数:", get_float())
    -print("浮点数范围:", get_float(min_size=1.0, max_size=2.0))
    -
    -# 随机时间
    -print("当前时间:", get_now_datetime())
    -print("当前时间(格式化字符串):", get_now_datetime(strftime=True))
    -print("未来时间:", get_future_datetime())
    -print("未来时间(格式化字符串):", get_future_datetime(strftime=True))
    -print("过去时间:", get_past_datetime())
    -print("过去时间(格式化字符串):", get_past_datetime(strftime=True))
    -
    -# 随机数据
    -print("整型:", get_int())
    -print("整型32位:", get_int32())
    -print("整型64位:", get_int64())
    -print("MD5:", get_md5())
    -print("UUID:", get_uuid())
    -
    -print("单词:", get_word())
    -print("单词组(3个):", get_words(3))
    -
    -print("手机号:", get_phone())
    -print("手机号(移动):", get_phone(operator="mobile"))
    -print("手机号(联通):", get_phone(operator="unicom"))
    -print("手机号(电信):", get_phone(operator="telecom"))
    -
    -# 在线时间
    -print("当前时间戳:", online_timestamp())
    -print("当前日期时间:", online_now_datetime())
    -
    名字: Hayden
    -名字(男): Brantley
    -名字(女): Julia
    -名字(中文男): 觅儿
    -名字(中文女): 若星
    -姓: Lee
    -姓(中文): 白
    -姓名: Genesis
    -姓名(中文): 廉高义
    -生日: 2000-03-11
    -生日字符串: 1994-11-12
    -生日年龄范围: 1996-01-12
    -日期(当前): 2022-09-17
    -日期(昨天): 2022-09-16
    -日期(明天): 2022-09-18
    -数字(8位): 48285099
    -邮箱: melanie@yahoo.com
    -浮点数: 1.5315717275531858e+308
    -浮点数范围: 1.6682402084146244
    -当前时间: 2022-09-17 23:33:22.736031
    -当前时间(格式化字符串): 2022-09-17 23:33:22
    -未来时间: 2054-05-02 11:33:47.736031
    -未来时间(格式化字符串): 2070-08-28 16:38:45
    -过去时间: 2004-09-03 12:56:23.737031
    -过去时间(格式化字符串): 2006-12-06 07:58:37
    -整型: 7831034423589443450
    -整型32位: 1119927937
    -整型64位: 3509365234787490389
    -MD5: d0f6c6abbfe1cfeea60ecfdd1ef2f4b9
    -UUID: 5fd50475-2723-4a36-a769-1d4c9784223a
    -单词: habitasse
    -单词组(3个): уж pede. metus.
    -手机号: 13171039843
    -手机号(移动): 15165746029
    -手机号(联通): 16672812525
    -手机号(电信): 17345142737
    -
    -当前时间戳 1695137988672
    -当前日期时间 2023-09-19 23:39:48
    -

    用例的依赖

    在 seldom 1.8.0 版本实现了该功能。

    在编写用例的时候不推荐你编写依赖的用例,但是,有些时候我们并不能完全消除这些依赖。seldom 增加了用例依赖的方法。

    depend

    depend 装饰器用来设置依赖的用例。

    import seldom
    -from seldom import depend
    -
    -
    -class TestDepend(seldom.TestCase):
    -
    -    def test_001(self):
    -        print("test_001")
    -
    -    @depend("test_001")
    -    def test_002(self):
    -        print("test_002")
    -
    -    @depend("test_002")
    -    def test_003(self):
    -        print("test_003")
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(debug=True)
    -

    test_002 依赖于 test_001 , test_003又依赖于test_002。当被依赖的用例,错误、失败、跳过,那么依赖的用例自动跳过。

    if_depend

    if_depend 装饰器不会依赖用例的执行状态,可以自己定义是否要跳过依赖的用例。

    import seldom
    -from seldom import if_depend
    -
    -
    -class TestIfDepend(seldom.TestCase):
    -    Test001 = True
    -
    -    def test_001(self):
    -        TestIfDepend.Test001 = False  # 修改Test001为 False
    -
    -    @if_depend("Test001")
    -    def test_002(self):
    -        ...
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(debug=True)
    -
    1. 首先,定义变量 Test001,默认值为True
    2. test_001用例中,可以根据一些条件来选择是否修改Test001的值,如果改为False, 那么依赖的用例将被跳过。
    3. test_002用例中,通过if_depend装饰器来判断Test001的值,如果为为False, 那么装饰的用例跳过,否则执行。

    @depend 和 @data()

    @depend() 装饰器可以和 @data() 装饰器混合使用。

    import seldom
    -from seldom import data, depend
    -
    -
    -class DataDriverTest(seldom.TestCase):
    -
    -    def test_001(self):
    -        self.assertEqual(1, 2)
    -
    -    @data([
    -        ("First", "seldom"),
    -        ("Second", "selenium"),
    -        ("Third", "unittest"),
    -    ])
    -    @depend("test_001")  # 依赖 test_001 的结果
    -    def test_002(self, name, keyword):
    -        """
    -        Used tuple test data
    -        :param name: case desc
    -        :param keyword: case data
    -        """
    -        print(f"{name} - test data: {keyword}")
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(debug=True)
    -

    使用要求:

    1. 被依赖的用例不能用 @data() 装饰器,否则就是一组用例了,只能指定单个用例。
    2. @depend() 要放到 @data() 下面使用。

    用例分类标签

    在 seldom 2.4.0 版本实现了该功能。

    使用方式

    # test_label.py
    -import seldom
    -from seldom import label
    -
    -
    -class MyTest(seldom.TestCase):
    -
    -    @label("base")
    -    def test_label_base(self):
    -        self.assertEqual(1 + 1, 2)
    -
    -    @label("slow")
    -    def test_label_slow(self):
    -        self.assertEqual(1, 2)
    -
    -    def test_no_label(self):
    -        self.assertEqual(2 + 3, 5)
    -
    -
    -if __name__ == '__main__':
    -    # seldom.main(debug=True, whitelist=["base"])  # whitelist
    -    seldom.main(debug=True, blacklist=["slow"])  # blacklist
    -

    如果只运行标签为base的用例,设置白名单(whitelist)。

    > python test_label.py
    -
    -test_label_base (btest_label.MyTest) ... ok
    -test_label_slow (btest_label.MyTest) ... skipped "label whitelist {'base'}"
    -test_no_label (btest_label.MyTest) ... skipped "label whitelist {'base'}"
    -
    -----------------------------------------------------------------------
    -Ran 3 tests in 0.001s
    -
    -OK (skipped=2)
    -

    如果只想屏蔽标签为slow的用例,设置黑名单(blacklist)。

    > python test_label.py
    -
    -test_label_base (btest_label.MyTest) ... ok
    -test_label_slow (btest_label.MyTest) ... skipped "label blacklist {'slow'}"
    -test_no_label (btest_label.MyTest) ... ok
    -----------------------------------------------------------------------
    -Ran 3 tests in 0.001s
    -
    -OK (skipped=1)
    -

    发送邮件

    在 seldom 1.2.4 版本实现了该功能。

    如果你想将测试完成的报告发送到指定邮箱,那么可以调用发邮件的方法实现。

    import seldom
    -from seldom import SMTP
    -
    -# ...
    -
    -if __name__ == '__main__':
    -    report_path = "/you/path/report.html"
    -    seldom.main(report=report_path)
    -    smtp = SMTP(user="send@126.com", password="abc123", host="smtp.126.com", ssl=True)
    -    smtp.sendmail(to="receive@mail.com", subject="Email title", attachments=report_path, delete=False)
    -

    SMTP()类

    sendmail()方法

    debug模式不会生成测试报告, 自动化发邮件不支持debug 模式,自然也无法将报告发送到指定邮箱了。

    发送钉钉

    在 seldom 2.6.0 版本实现了该功能。

    seldom 还提供了发送钉钉的 API。

    帮助文档: https://open.dingtalk.com/document/group/enterprise-created-chatbot

    import seldom
    -from seldom import DingTalk
    -
    -# ...
    -
    -if __name__ == '__main__':
    -    seldom.main()
    -    ding = DingTalk(
    -        access_token="690900b5ce6d5d10bb1218b8e64a4e2b55f96a6d116aaf50",
    -        key="xxxx",
    -        app_secret="xxxxx",
    -        at_mobiles=[13700000000, 13800000000],
    -        is_at_all=False,
    -    )
    -    ding.sender()
    -

    参数说明:

    seldom日志

    在 seldom 2.9.0 版本提供了日志的配置能力。

    在项目中你可以使用seldom提供的log 打印日志。

    from seldom.logging import log
    -
    -log.trace("this is trace info.")
    -log.info("this is info.")
    -log.error("this error info.")
    -log.debug("this debug info.")
    -log.success("this success info.")
    -log.warning("this warning info.")
    -
    
    -2022-04-30 16:31:49 test_log.py | TRACE | this is trace info.
    -2022-04-30 16:31:49 test_log.py | INFO | this is info.
    -2022-04-30 16:31:49 test_log.py | ERROR | this error info.
    -2022-04-30 16:31:49 test_log.py | DEBUG | this debug info.
    -2022-04-30 16:31:49 test_log.py | SUCCESS | this success info.
    -2022-04-30 16:31:49 test_log.py | WARNING | this warning info.
    -
    from seldom.logging import log_cfg
    -from seldom.logging import log
    -
    -log_cfg.set_level(colorlog=False)  # 关闭日志颜色
    -log.trace("this is trace info.")
    -# ...
    -
    from seldom.logging import log_cfg
    -from seldom.logging import log
    -
    -# 定义日志格式
    -format = "<green>{time:YYYY-MM-DD HH:mm:ss}</> {file} |<level> {level} | {message}</level>"
    -log_cfg.set_level(format=format)
    -log.trace("this is trace info.")
    -
    from seldom.logging import log_cfg
    -from seldom.logging import log
    -
    -# 设置日志级别
    -log_cfg.set_level(level="DEBUG")
    -log.trace("this is trace info.")
    -log.error("this error info.")
    -

    log level: TRACE < DEBUG < INFO < SUCCESS < WARNING < ERROR

    缓存 cache

    在 seldom 2.10.0 版本实现了该功能。

    实际测试过程中,往往需要需要通过cache去记录一些数据,从而减少不必要的操作。例如 登录token,很多条用例都会用到登录token,那么就可以借助缓存来暂存登录token,从而减少重复动作。

    from seldom.utils import cache
    -
    -# 清除指定缓存
    -cache.clear()
    -
    -# 获取指定缓存
    -token = cache.get("token")
    -print(f"token: {token}")
    -
    -# 判断为空写入缓存
    -if token is None:
    -    cache.set({"token": "123"})
    -
    -# 设置存在的数据(相当于更新)
    -cache.set({"token": "456"})
    -
    -# value复杂格式设置存在的数据
    -cache.set({"user": [{"name": "tom", "age": 11}]})
    -
    -# 获取所有缓存
    -all_token = cache.get()
    -print(f"all: {all_token}")
    -
    -# 清除指定缓存
    -cache.clear("token")
    -

    注:seldom 提供的 cache 本质上是通过json文件来临时记录数据,没有失效时间。你需要在适当的位置做清除操作。例如,整个用例开始时清除。

    使用内存的实现的cache 装饰器。

    import time
    -import seldom
    -from seldom.utils import memory_cache
    -
    -
    -@memory_cache()
    -def add(x, y):
    -    print("calculating: %s + %s" % (x, y))
    -    time.sleep(2)
    -    c = x + y
    -    return c
    -
    -
    -class MyTest(seldom.TestCase):
    -
    -    def test_case(self):
    -        """test cache 1"""
    -        r = add(1, 2)
    -        self.assertEqual(r, 3)
    -
    -    def test_case2(self):
    -        """test cache 2"""
    -        r = add(1, 2)
    -        self.assertEqual(r, 3)
    -
    -    def test_case3(self):
    -        """test cache 3"""
    -        r = add(1, 2)
    -        self.assertEqual(r, 3)
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(debug=True)
    -

    使用磁盘实现的cache 装饰器。

    import time
    -import seldom
    -from seldom.utils import disk_cache
    -
    -
    -@disk_cache()
    -def add(x, y):
    -    print("calculating: %s + %s" % (x, y))
    -    time.sleep(2)
    -    c = x + y
    -    return c
    -
    -
    -class MyTest(seldom.TestCase):
    -
    -    def test_case(self):
    -        """test cache 1"""
    -        r = add(1, 2)
    -        self.assertEqual(r, 3)
    -
    -    def test_case2(self):
    -        """test cache 2"""
    -        r = add(1, 2)
    -        self.assertEqual(r, 3)
    -
    -    def test_case3(self):
    -        """test cache 3"""
    -        r = add(1, 2)
    -        self.assertEqual(r, 3)
    -
    -
    -if __name__ == '__main__':
    -    dc = disk_cache()
    -    # 清除所有函数缓存
    -    # dc.clear()
    -    # 清除 \`add()\` 函数缓存
    -    dc.clear("add")
    -    seldom.main(debug=True)
    -
    `,96),o=[p];function c(l,i){return s(),a("div",null,o)}const d=n(e,[["render",c],["__file","advanced.html.vue"]]);export{d as default}; diff --git a/assets/api_case.html-69671d2d.js b/assets/api_case.html-69671d2d.js deleted file mode 100644 index d975a66..0000000 --- a/assets/api_case.html-69671d2d.js +++ /dev/null @@ -1,131 +0,0 @@ -import{_ as t,r as e,o,c as p,a as n,b as s,d as i,e as l}from"./app-a06a2d51.js";const c="/image/api_excel_report.png",u={},d=n("h1",{id:"支持excel测试用例",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#支持excel测试用例","aria-hidden":"true"},"#"),s(" 支持Excel测试用例")],-1),r=n("blockquote",null,[n("p",null,"seldom > 3.8.0")],-1),k=n("p",null,[s("在编写接口测试用例的时候,有时候测试用例非常简单,比如单接口的测试,不需要登录token,不存在用例数据依赖,也不需要参数加密,此时,使用"),n("code",null,"Excel"),s(" 文件编写用例更为高效。")],-1),v=n("p",null,"seldom支持了这种用例的编写。",-1),m=n("h3",{id:"编写excel用例",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#编写excel用例","aria-hidden":"true"},"#"),s(" 编写Excel用例")],-1),b={href:"https://github.com/SeldomQA/seldom/tree/master/api_case",target:"_blank",rel:"noopener noreferrer"},q=l(`

    首先,创建一个Excel文件,格式如下。

    nameapimethodheadersparam_typeparamsassertexclude
    简单GET接口/getGET{}data{}{}[]
    简单POST接口-json参数/postPOST{}json{}{}[]
    ...

    参数说明

    字段说明列子
    name用例的名称,会在测试报告中展示。
    api接口的地址,可以写完整的URL地址, 也可以只定义路径,base_urlconfrun.py例如:http://www.httpbin.org/get or /get
    method接口的请求方法,必须大写,不允许为空支持:GETPOSTPUTDELETE
    headers请求头,不允许为空,默认为 {},字段必须双引号"例如:{"user-agent": "my-app/0.0.1"}
    param_type接口参数类型,必须小写,不允许为空。例如:datajson
    params接口参数,不允许为空,默认为 {},字段必须双引号"例如:{"id": 1, "name": "jack"}
    assert断言接口返回,允许为空 或 {}例如:{"status": 200, "success": True, "data": [...]}
    exclude断言过滤字段,一些特殊的字段会导致断言失败,需要过滤掉。例如:["X-Amzn-Trace-Id", "timestamp"]

    confrun.py配置

    
    -def base_url():
    -    """
    -    http test
    -    api base url
    -    """
    -    return "http://www.httpbin.org"
    -
    -
    -def debug():
    -    """
    -    debug mod
    -    """
    -    return False
    -
    -
    -def rerun():
    -    """
    -    error/failure rerun times
    -    """
    -    return 0
    -
    -
    -def report():
    -    """
    -    setting report path
    -    Used:
    -    return "d://mypro/result.html" or "d://mypro/result.xml"
    -    """
    -    return None
    -
    -
    -def timeout():
    -    """
    -    setting timeout
    -    """
    -    return 10
    -
    -
    -def title():
    -    """
    -    setting report title
    -    """
    -    return "seldom 执行 excel 接口用例"
    -
    -
    -def tester():
    -    """
    -    setting report tester
    -    """
    -    return "bugmaster"
    -
    -
    -def description():
    -    """
    -    setting report description
    -    """
    -    return ["windows", "api"]
    -
    -
    -def language():
    -    """
    -    setting report language
    -    return "en" or "zh-CN"
    -    """
    -    return "zh-CN"
    -
    -
    -def failfast():
    -    """
    -    fail fast
    -    :return:
    -    """
    -    return False
    -

    运行测试用例

    mypro/
    -├── api_case.xlsx
    -└── confrun.py
    -
    > cd mypro
    -> seldom --api-excel api_case.xlsx
    -
    
    - seldom --api-excel .\\api_case.xlsx
    -run .\\api_case.xlsx file.
    -
    -              __    __
    -   ________  / /___/ /___  ____ ____
    -  / ___/ _ \\/ / __  / __ \\/ __ \` ___/
    - (__  )  __/ / /_/ / /_/ / / / / / /
    -/____/\\___/_/\\__,_/\\____/_/ /_/ /_/  v3.x.x
    ------------------------------------------
    -                             @itest.info
    -
    -2024-07-06 21:00:35 | INFO     | runner.py | TestLoader: ...\\Lib\\site-packages\\seldom\\file_runner\\api_excel.py
    -2024-07-06 21:00:35 | INFO     | parameterization.py | find data file: .\\api_case.xlsx
    -
    -XTestRunner Running tests...
    -
    -----------------------------------------------------------------------
    -2024-07-06 21:00:35 | INFO     | api_excel.py | execute api case: [简单GET接口]
    -2024-07-06 21:00:35 | INFO     | request.py | -------------- Request -----------------[🚀]
    -2024-07-06 21:00:35 | INFO     | request.py | [method]: GET      [url]: http://www.httpbin.org/get
    -2024-07-06 21:00:35 | DEBUG    | request.py | [headers]:
    -{
    -  "user-agent": "my-app/0.0.1"
    -}
    -2024-07-06 21:00:35 | DEBUG    | request.py | [params]:
    -{
    -  "key": "value"
    -}
    -2024-07-06 21:00:35 | INFO     | request.py | -------------- Response ----------------[🛬️]
    -2024-07-06 21:00:35 | INFO     | request.py | successful with status 200
    -2024-07-06 21:00:35 | DEBUG    | request.py | [type]: json      [time]: 0.481752
    -2024-07-06 21:00:35 | DEBUG    | request.py | [response]:
    - {
    -  "args": {
    -    "key": "value"
    -  },
    -  "headers": {
    -    "Accept": "*/*",
    -    "Accept-Encoding": "gzip, deflate",
    -    "Host": "www.httpbin.org",
    -    "User-Agent": "my-app/0.0.1",
    -    "X-Amzn-Trace-Id": "Root=1-66893ff2-60ed7c5378ca01452917ea0c"
    -  },
    -  "origin": "14.155.89.115",
    -  "url": "http://www.httpbin.org/get?key=value"
    -}
    -2024-07-06 21:00:35 | INFO     | case.py | 👀 assertJSON -> {'args': {'key': 'value'}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'www.httpbin.org', 'User-Agent': 'my-app/0.0.1', 'X-Amzn-Trace-Id': 'Root=1-668906ef-2e2d8c4c3f36a228264da1ab'}, 'origin': '14.155.89.115', 'url': 'http://www.httpbin.org/get?key=value'}.
    -
    -...
    -
    -

    ',15);function g(_,h){const a=e("ExternalLinkIcon");return o(),p("div",null,[d,r,k,v,m,n("p",null,[n("a",b,[s("查看例子"),i(a)])]),q])}const f=t(u,[["render",g],["__file","api_case.html.vue"]]);export{f as default}; diff --git a/assets/api_case.html-1c2fbfd8.js b/assets/api_case.html-8b6be0a5.js similarity index 99% rename from assets/api_case.html-1c2fbfd8.js rename to assets/api_case.html-8b6be0a5.js index c85380c..e628b21 100644 --- a/assets/api_case.html-1c2fbfd8.js +++ b/assets/api_case.html-8b6be0a5.js @@ -1,4 +1,4 @@ -import{_ as t,r as e,o,c as p,a as n,b as s,d as i,e as l}from"./app-72107ff1.js";const c="/image/api_excel_report.png",u={},d=n("h1",{id:"支持excel测试用例",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#支持excel测试用例","aria-hidden":"true"},"#"),s(" 支持Excel测试用例")],-1),r=n("blockquote",null,[n("p",null,"seldom > 3.8.0")],-1),k=n("p",null,[s("在编写接口测试用例的时候,有时候测试用例非常简单,比如单接口的测试,不需要登录token,不存在用例数据依赖,也不需要参数加密,此时,使用"),n("code",null,"Excel"),s(" 文件编写用例更为高效。")],-1),v=n("p",null,"seldom支持了这种用例的编写。",-1),m=n("h3",{id:"编写excel用例",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#编写excel用例","aria-hidden":"true"},"#"),s(" 编写Excel用例")],-1),b={href:"https://github.com/SeldomQA/seldom/tree/master/api_case",target:"_blank",rel:"noopener noreferrer"},q=l(`

    首先,创建一个Excel文件,格式如下。

    nameapimethodheadersparam_typeparamsassertexclude
    简单GET接口/getGET{}data{}{}[]
    简单POST接口-json参数/postPOST{}json{}{}[]
    ...

    参数说明

    字段说明列子
    name用例的名称,会在测试报告中展示。
    api接口的地址,可以写完整的URL地址, 也可以只定义路径,base_urlconfrun.py例如:http://www.httpbin.org/get or /get
    method接口的请求方法,必须大写,不允许为空支持:GETPOSTPUTDELETE
    headers请求头,不允许为空,默认为 {},字段必须双引号"例如:{"user-agent": "my-app/0.0.1"}
    param_type接口参数类型,必须小写,不允许为空。例如:datajson
    params接口参数,不允许为空,默认为 {},字段必须双引号"例如:{"id": 1, "name": "jack"}
    assert断言接口返回,允许为空 或 {}例如:{"status": 200, "success": True, "data": [...]}
    exclude断言过滤字段,一些特殊的字段会导致断言失败,需要过滤掉。例如:["X-Amzn-Trace-Id", "timestamp"]

    confrun.py配置

    
    +import{_ as t,r as e,o,c as p,a as n,b as s,d as i,e as l}from"./app-9fb6f1b5.js";const c="/image/api_excel_report.png",u={},d=n("h1",{id:"支持excel测试用例",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#支持excel测试用例","aria-hidden":"true"},"#"),s(" 支持Excel测试用例")],-1),r=n("blockquote",null,[n("p",null,"seldom > 3.8.0")],-1),k=n("p",null,[s("在编写接口测试用例的时候,有时候测试用例非常简单,比如单接口的测试,不需要登录token,不存在用例数据依赖,也不需要参数加密,此时,使用"),n("code",null,"Excel"),s(" 文件编写用例更为高效。")],-1),v=n("p",null,"seldom支持了这种用例的编写。",-1),m=n("h3",{id:"编写excel用例",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#编写excel用例","aria-hidden":"true"},"#"),s(" 编写Excel用例")],-1),b={href:"https://github.com/SeldomQA/seldom/tree/master/api_case",target:"_blank",rel:"noopener noreferrer"},q=l(`

    首先,创建一个Excel文件,格式如下。

    nameapimethodheadersparam_typeparamsassertexclude
    简单GET接口/getGET{}data{}{}[]
    简单POST接口-json参数/postPOST{}json{}{}[]
    ...

    参数说明

    字段说明列子
    name用例的名称,会在测试报告中展示。
    api接口的地址,可以写完整的URL地址, 也可以只定义路径,base_urlconfrun.py例如:http://www.httpbin.org/get or /get
    method接口的请求方法,必须大写,不允许为空支持:GETPOSTPUTDELETE
    headers请求头,不允许为空,默认为 {},字段必须双引号"例如:{"user-agent": "my-app/0.0.1"}
    param_type接口参数类型,必须小写,不允许为空。例如:datajson
    params接口参数,不允许为空,默认为 {},字段必须双引号"例如:{"id": 1, "name": "jack"}
    assert断言接口返回,允许为空 或 {}例如:{"status": 200, "success": True, "data": [...]}
    exclude断言过滤字段,一些特殊的字段会导致断言失败,需要过滤掉。例如:["X-Amzn-Trace-Id", "timestamp"]

    confrun.py配置

    
     def base_url():
         """
         http test
    diff --git a/assets/api_object.html-3082ec6d.js b/assets/api_object.html-331a6734.js
    similarity index 99%
    rename from assets/api_object.html-3082ec6d.js
    rename to assets/api_object.html-331a6734.js
    index c4f3ba0..a79bceb 100644
    --- a/assets/api_object.html-3082ec6d.js
    +++ b/assets/api_object.html-331a6734.js
    @@ -1,4 +1,4 @@
    -import{_ as n,o as s,c as a,e as t}from"./app-72107ff1.js";const e={},p=t(`

    API Object

    API Object Models,简称AOM,AOM是一种设计模式,它围绕着将API、路由或功能交互及其相关行为封装在结构良好的对象中。AOM旨在增强API测试和集成的直观性和弹性。在实践中,AOM需要精心设计专门的API对象,以有效地保护用户免受与API 请求、响应、端点交互和身份验证过程相关的复杂性的影响。

    seldom 支持AOM, 并且提供了一些好用的功能,辅助你使用AOM.

    • 目录结构如下
    mypro/
    +import{_ as n,o as s,c as a,e as t}from"./app-9fb6f1b5.js";const e={},p=t(`

    API Object

    API Object Models,简称AOM,AOM是一种设计模式,它围绕着将API、路由或功能交互及其相关行为封装在结构良好的对象中。AOM旨在增强API测试和集成的直观性和弹性。在实践中,AOM需要精心设计专门的API对象,以有效地保护用户免受与API 请求、响应、端点交互和身份验证过程相关的复杂性的影响。

    seldom 支持AOM, 并且提供了一些好用的功能,辅助你使用AOM.

    • 目录结构如下
    mypro/
     ├── api/
     │   ├── __init__.py
     │   ├── auth_object.py
    diff --git a/assets/api_object.html-931137ea.js b/assets/api_object.html-931137ea.js
    deleted file mode 100644
    index e177d18..0000000
    --- a/assets/api_object.html-931137ea.js
    +++ /dev/null
    @@ -1,46 +0,0 @@
    -import{_ as n,o as s,c as a,e as t}from"./app-a06a2d51.js";const e={},p=t(`

    API Object

    API Object Models,简称AOM,AOM是一种设计模式,它围绕着将API、路由或功能交互及其相关行为封装在结构良好的对象中。AOM旨在增强API测试和集成的直观性和弹性。在实践中,AOM需要精心设计专门的API对象,以有效地保护用户免受与API 请求、响应、端点交互和身份验证过程相关的复杂性的影响。

    seldom 支持AOM, 并且提供了一些好用的功能,辅助你使用AOM.

    • 目录结构如下
    mypro/
    -├── api/
    -│   ├── __init__.py
    -│   ├── auth_object.py
    -│   └── xxx_object.py
    -├── test_dir/
    -│   ├── test_auth.py
    -│   └── test_xxx.py
    -│  ...
    -
    • 创建 API Object
    # api/auth_object.py
    -from seldom.testdata import get_int
    -from seldom.request import HttpRequest
    -from seldom.request import check_response
    -
    -
    -class AuthAPIObject(HttpRequest):
    -
    -    def __init__(self, api_key):
    -        self.api_key = api_key
    -
    -    @check_response(ret="form.token")
    -    def get_token(self, user_id:str) -> str:
    -        """
    -        模拟:根据用户ID生成登录token
    -        :param user_id:
    -        :return:
    -        """
    -        data = {"user_id": user_id, "token": "t" + str(get_int(10000, 99999))}
    -        r = self.post("/post?key=" + self.api_key, data=data)
    -        return r
    -

    定义API接口,根据get_token()用于生成token,这里我们通过随机数模拟的生成的token。check_response()装饰器用于装饰接口,form.token 用于提取API的返回值。

    • 创建测试用例
    # test_dir/test_auth.py
    -import seldom
    -from api.auth_object import AuthAPIObject
    -
    -
    -class TestAPI(seldom.TestCase):
    -
    -    def test_case(self):
    -        auth_object = AuthAPIObject(api_key="abc123")
    -        token = auth_object.get_token(user_id="123")
    -        print("token", token)
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(debug=True, base_url="https://httpbin.org")
    -

    在用例层调用AuthAPIObject类下面的对象,测试API。

    • AOM 原则

    首先,API只允许通过的APIObject进行封装,那么在封装之前可以检索一下是否有封装了,如果有,进一步确认是否满足自己的调用需求,我们一般在测试API的时候一般各种参数验证,当API作为依赖接口调用的时候,一般参数比较少且固定,所以,API在封装的时候要兼顾到这两种情况。

    其次,用例层只能通过APIObject的封装调用API,像登录token这种大部分API会用到的信息,可以通过类初始化时传入,后续调用类下面方法的时候就不需要关心的。如果是多个API组成一个场景,也可以再进行一层业务层的封装。

    `,14),o=[p];function c(i,l){return s(),a("div",null,o)}const r=n(e,[["render",c],["__file","api_object.html.vue"]]);export{r as default}; diff --git a/assets/app-72107ff1.js b/assets/app-9fb6f1b5.js similarity index 84% rename from assets/app-72107ff1.js rename to assets/app-9fb6f1b5.js index 1fa5a62..f45f188 100644 --- a/assets/app-72107ff1.js +++ b/assets/app-9fb6f1b5.js @@ -1,4 +1,4 @@ -const Qi="modulepreload",Zi=function(e){return"/"+e},Jo={},W=function(t,n,r){if(!n||n.length===0)return t();const o=document.getElementsByTagName("link");return Promise.all(n.map(s=>{if(s=Zi(s),s in Jo)return;Jo[s]=!0;const l=s.endsWith(".css"),i=l?'[rel="stylesheet"]':"";if(!!r)for(let u=o.length-1;u>=0;u--){const f=o[u];if(f.href===s&&(!l||f.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${s}"]${i}`))return;const c=document.createElement("link");if(c.rel=l?"stylesheet":Qi,l||(c.as="script",c.crossOrigin=""),c.href=s,document.head.appendChild(c),l)return new Promise((u,f)=>{c.addEventListener("load",u),c.addEventListener("error",()=>f(new Error(`Unable to preload CSS for ${s}`)))})})).then(()=>t())};function mo(e,t){const n=Object.create(null),r=e.split(",");for(let o=0;o!!n[o.toLowerCase()]:o=>!!n[o]}const xe={},rn=[],ot=()=>{},Xi=()=>!1,ea=/^on[^a-z]/,Fn=e=>ea.test(e),go=e=>e.startsWith("onUpdate:"),Ae=Object.assign,vo=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},ta=Object.prototype.hasOwnProperty,de=(e,t)=>ta.call(e,t),Q=Array.isArray,on=e=>kr(e)==="[object Map]",pl=e=>kr(e)==="[object Set]",oe=e=>typeof e=="function",ge=e=>typeof e=="string",_o=e=>typeof e=="symbol",ke=e=>e!==null&&typeof e=="object",ml=e=>ke(e)&&oe(e.then)&&oe(e.catch),gl=Object.prototype.toString,kr=e=>gl.call(e),na=e=>kr(e).slice(8,-1),vl=e=>kr(e)==="[object Object]",bo=e=>ge(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,Ln=mo(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),xr=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},ra=/-(\w)/g,ft=xr(e=>e.replace(ra,(t,n)=>n?n.toUpperCase():"")),oa=/\B([A-Z])/g,Yt=xr(e=>e.replace(oa,"-$1").toLowerCase()),Lr=xr(e=>e.charAt(0).toUpperCase()+e.slice(1)),Mr=xr(e=>e?`on${Lr(e)}`:""),On=(e,t)=>!Object.is(e,t),Hr=(e,t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,value:n})},sa=e=>{const t=parseFloat(e);return isNaN(t)?e:t},la=e=>{const t=ge(e)?Number(e):NaN;return isNaN(t)?e:t};let Qo;const Zr=()=>Qo||(Qo=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function jn(e){if(Q(e)){const t={};for(let n=0;n{if(n){const r=n.split(aa);r.length>1&&(t[r[0].trim()]=r[1].trim())}}),t}function qe(e){let t="";if(ge(e))t=e;else if(Q(e))for(let n=0;nge(e)?e:e==null?"":Q(e)||ke(e)&&(e.toString===gl||!oe(e.toString))?JSON.stringify(e,bl,2):String(e),bl=(e,t)=>t&&t.__v_isRef?bl(e,t.value):on(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[r,o])=>(n[`${r} =>`]=o,n),{})}:pl(t)?{[`Set(${t.size})`]:[...t.values()]}:ke(t)&&!Q(t)&&!vl(t)?String(t):t;let Ge;class ha{constructor(t=!1){this.detached=t,this._active=!0,this.effects=[],this.cleanups=[],this.parent=Ge,!t&&Ge&&(this.index=(Ge.scopes||(Ge.scopes=[])).push(this)-1)}get active(){return this._active}run(t){if(this._active){const n=Ge;try{return Ge=this,t()}finally{Ge=n}}}on(){Ge=this}off(){Ge=this.parent}stop(t){if(this._active){let n,r;for(n=0,r=this.effects.length;n{const t=new Set(e);return t.w=0,t.n=0,t},El=e=>(e.w&Rt)>0,wl=e=>(e.n&Rt)>0,ga=({deps:e})=>{if(e.length)for(let t=0;t{const{deps:t}=e;if(t.length){let n=0;for(let r=0;r{(u==="length"||u>=a)&&i.push(c)})}else switch(n!==void 0&&i.push(l.get(n)),t){case"add":Q(e)?bo(n)&&i.push(l.get("length")):(i.push(l.get(qt)),on(e)&&i.push(l.get(eo)));break;case"delete":Q(e)||(i.push(l.get(qt)),on(e)&&i.push(l.get(eo)));break;case"set":on(e)&&i.push(l.get(qt));break}if(i.length===1)i[0]&&to(i[0]);else{const a=[];for(const c of i)c&&a.push(...c);to(yo(a))}}function to(e,t){const n=Q(e)?e:[...e];for(const r of n)r.computed&&Xo(r);for(const r of n)r.computed||Xo(r)}function Xo(e,t){(e!==nt||e.allowRecurse)&&(e.scheduler?e.scheduler():e.run())}function _a(e,t){var n;return(n=ur.get(e))==null?void 0:n.get(t)}const ba=mo("__proto__,__v_isRef,__isVue"),Ll=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(_o)),ya=wo(),Ea=wo(!1,!0),wa=wo(!0),es=ka();function ka(){const e={};return["includes","indexOf","lastIndexOf"].forEach(t=>{e[t]=function(...n){const r=pe(this);for(let s=0,l=this.length;s{e[t]=function(...n){mn();const r=pe(this)[t].apply(this,n);return gn(),r}}),e}function xa(e){const t=pe(this);return We(t,"has",e),t.hasOwnProperty(e)}function wo(e=!1,t=!1){return function(r,o,s){if(o==="__v_isReactive")return!e;if(o==="__v_isReadonly")return e;if(o==="__v_isShallow")return t;if(o==="__v_raw"&&s===(e?t?ja:Al:t?Pl:Sl).get(r))return r;const l=Q(r);if(!e){if(l&&de(es,o))return Reflect.get(es,o,s);if(o==="hasOwnProperty")return xa}const i=Reflect.get(r,o,s);return(_o(o)?Ll.has(o):ba(o))||(e||We(r,"get",o),t)?i:Ie(i)?l&&bo(o)?i:i.value:ke(i)?e?Bn(i):vn(i):i}}const La=Cl(),Ca=Cl(!0);function Cl(e=!1){return function(n,r,o,s){let l=n[r];if(cn(l)&&Ie(l)&&!Ie(o))return!1;if(!e&&(!fr(o)&&!cn(o)&&(l=pe(l),o=pe(o)),!Q(n)&&Ie(l)&&!Ie(o)))return l.value=o,!0;const i=Q(n)&&bo(r)?Number(r)e,Cr=e=>Reflect.getPrototypeOf(e);function Gn(e,t,n=!1,r=!1){e=e.__v_raw;const o=pe(e),s=pe(t);n||(t!==s&&We(o,"get",t),We(o,"get",s));const{has:l}=Cr(o),i=r?ko:n?Co:Rn;if(l.call(o,t))return i(e.get(t));if(l.call(o,s))return i(e.get(s));e!==o&&e.get(t)}function Yn(e,t=!1){const n=this.__v_raw,r=pe(n),o=pe(e);return t||(e!==o&&We(r,"has",e),We(r,"has",o)),e===o?n.has(e):n.has(e)||n.has(o)}function Jn(e,t=!1){return e=e.__v_raw,!t&&We(pe(e),"iterate",qt),Reflect.get(e,"size",e)}function ts(e){e=pe(e);const t=pe(this);return Cr(t).has.call(t,e)||(t.add(e),_t(t,"add",e,e)),this}function ns(e,t){t=pe(t);const n=pe(this),{has:r,get:o}=Cr(n);let s=r.call(n,e);s||(e=pe(e),s=r.call(n,e));const l=o.call(n,e);return n.set(e,t),s?On(t,l)&&_t(n,"set",e,t):_t(n,"add",e,t),this}function rs(e){const t=pe(this),{has:n,get:r}=Cr(t);let o=n.call(t,e);o||(e=pe(e),o=n.call(t,e)),r&&r.call(t,e);const s=t.delete(e);return o&&_t(t,"delete",e,void 0),s}function os(){const e=pe(this),t=e.size!==0,n=e.clear();return t&&_t(e,"clear",void 0,void 0),n}function Qn(e,t){return function(r,o){const s=this,l=s.__v_raw,i=pe(l),a=t?ko:e?Co:Rn;return!e&&We(i,"iterate",qt),l.forEach((c,u)=>r.call(o,a(c),a(u),s))}}function Zn(e,t,n){return function(...r){const o=this.__v_raw,s=pe(o),l=on(s),i=e==="entries"||e===Symbol.iterator&&l,a=e==="keys"&&l,c=o[e](...r),u=n?ko:t?Co:Rn;return!t&&We(s,"iterate",a?eo:qt),{next(){const{value:f,done:h}=c.next();return h?{value:f,done:h}:{value:i?[u(f[0]),u(f[1])]:u(f),done:h}},[Symbol.iterator](){return this}}}}function kt(e){return function(...t){return e==="delete"?!1:this}}function Ra(){const e={get(s){return Gn(this,s)},get size(){return Jn(this)},has:Yn,add:ts,set:ns,delete:rs,clear:os,forEach:Qn(!1,!1)},t={get(s){return Gn(this,s,!1,!0)},get size(){return Jn(this)},has:Yn,add:ts,set:ns,delete:rs,clear:os,forEach:Qn(!1,!0)},n={get(s){return Gn(this,s,!0)},get size(){return Jn(this,!0)},has(s){return Yn.call(this,s,!0)},add:kt("add"),set:kt("set"),delete:kt("delete"),clear:kt("clear"),forEach:Qn(!0,!1)},r={get(s){return Gn(this,s,!0,!0)},get size(){return Jn(this,!0)},has(s){return Yn.call(this,s,!0)},add:kt("add"),set:kt("set"),delete:kt("delete"),clear:kt("clear"),forEach:Qn(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(s=>{e[s]=Zn(s,!1,!1),n[s]=Zn(s,!0,!1),t[s]=Zn(s,!1,!0),r[s]=Zn(s,!0,!0)}),[e,n,t,r]}const[Ia,$a,Na,Da]=Ra();function xo(e,t){const n=t?e?Da:Na:e?$a:Ia;return(r,o,s)=>o==="__v_isReactive"?!e:o==="__v_isReadonly"?e:o==="__v_raw"?r:Reflect.get(de(n,o)&&o in r?n:r,o,s)}const Ma={get:xo(!1,!1)},Ha={get:xo(!1,!0)},Fa={get:xo(!0,!1)},Sl=new WeakMap,Pl=new WeakMap,Al=new WeakMap,ja=new WeakMap;function Ba(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function za(e){return e.__v_skip||!Object.isExtensible(e)?0:Ba(na(e))}function vn(e){return cn(e)?e:Lo(e,!1,Tl,Ma,Sl)}function Va(e){return Lo(e,!1,Oa,Ha,Pl)}function Bn(e){return Lo(e,!0,Aa,Fa,Al)}function Lo(e,t,n,r,o){if(!ke(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const s=o.get(e);if(s)return s;const l=za(e);if(l===0)return e;const i=new Proxy(e,l===2?r:n);return o.set(e,i),i}function sn(e){return cn(e)?sn(e.__v_raw):!!(e&&e.__v_isReactive)}function cn(e){return!!(e&&e.__v_isReadonly)}function fr(e){return!!(e&&e.__v_isShallow)}function Ol(e){return sn(e)||cn(e)}function pe(e){const t=e&&e.__v_raw;return t?pe(t):e}function Rl(e){return cr(e,"__v_skip",!0),e}const Rn=e=>ke(e)?vn(e):e,Co=e=>ke(e)?Bn(e):e;function To(e){At&&nt&&(e=pe(e),xl(e.dep||(e.dep=yo())))}function So(e,t){e=pe(e);const n=e.dep;n&&to(n)}function Ie(e){return!!(e&&e.__v_isRef===!0)}function me(e){return $l(e,!1)}function Il(e){return $l(e,!0)}function $l(e,t){return Ie(e)?e:new Ua(e,t)}class Ua{constructor(t,n){this.__v_isShallow=n,this.dep=void 0,this.__v_isRef=!0,this._rawValue=n?t:pe(t),this._value=n?t:Rn(t)}get value(){return To(this),this._value}set value(t){const n=this.__v_isShallow||fr(t)||cn(t);t=n?t:pe(t),On(t,this._rawValue)&&(this._rawValue=t,this._value=n?t:Rn(t),So(this))}}function ee(e){return Ie(e)?e.value:e}const qa={get:(e,t,n)=>ee(Reflect.get(e,t,n)),set:(e,t,n,r)=>{const o=e[t];return Ie(o)&&!Ie(n)?(o.value=n,!0):Reflect.set(e,t,n,r)}};function Nl(e){return sn(e)?e:new Proxy(e,qa)}class Wa{constructor(t){this.dep=void 0,this.__v_isRef=!0;const{get:n,set:r}=t(()=>To(this),()=>So(this));this._get=n,this._set=r}get value(){return this._get()}set value(t){this._set(t)}}function Ka(e){return new Wa(e)}function Tr(e){const t=Q(e)?new Array(e.length):{};for(const n in e)t[n]=Dl(e,n);return t}class Ga{constructor(t,n,r){this._object=t,this._key=n,this._defaultValue=r,this.__v_isRef=!0}get value(){const t=this._object[this._key];return t===void 0?this._defaultValue:t}set value(t){this._object[this._key]=t}get dep(){return _a(pe(this._object),this._key)}}class Ya{constructor(t){this._getter=t,this.__v_isRef=!0,this.__v_isReadonly=!0}get value(){return this._getter()}}function Ja(e,t,n){return Ie(e)?e:oe(e)?new Ya(e):ke(e)&&arguments.length>1?Dl(e,t,n):me(e)}function Dl(e,t,n){const r=e[t];return Ie(r)?r:new Ga(e,t,n)}class Qa{constructor(t,n,r,o){this._setter=n,this.dep=void 0,this.__v_isRef=!0,this.__v_isReadonly=!1,this._dirty=!0,this.effect=new Eo(t,()=>{this._dirty||(this._dirty=!0,So(this))}),this.effect.computed=this,this.effect.active=this._cacheable=!o,this.__v_isReadonly=r}get value(){const t=pe(this);return To(t),(t._dirty||!t._cacheable)&&(t._dirty=!1,t._value=t.effect.run()),t._value}set value(t){this._setter(t)}}function Za(e,t,n=!1){let r,o;const s=oe(e);return s?(r=e,o=ot):(r=e.get,o=e.set),new Qa(r,o,s||!o,n)}function Ot(e,t,n,r){let o;try{o=r?e(...r):e()}catch(s){zn(s,t,n)}return o}function Xe(e,t,n,r){if(oe(e)){const s=Ot(e,t,n,r);return s&&ml(s)&&s.catch(l=>{zn(l,t,n)}),s}const o=[];for(let s=0;s>>1;$n(je[r])ut&&je.splice(t,1)}function nc(e){Q(e)?ln.push(...e):(!mt||!mt.includes(e,e.allowRecurse?Bt+1:Bt))&&ln.push(e),Hl()}function ss(e,t=In?ut+1:0){for(;t$n(n)-$n(r)),Bt=0;Bte.id==null?1/0:e.id,rc=(e,t)=>{const n=$n(e)-$n(t);if(n===0){if(e.pre&&!t.pre)return-1;if(t.pre&&!e.pre)return 1}return n};function Fl(e){no=!1,In=!0,je.sort(rc);const t=ot;try{for(ut=0;utge(g)?g.trim():g)),f&&(o=n.map(sa))}let i,a=r[i=Mr(t)]||r[i=Mr(ft(t))];!a&&s&&(a=r[i=Mr(Yt(t))]),a&&Xe(a,e,6,o);const c=r[i+"Once"];if(c){if(!e.emitted)e.emitted={};else if(e.emitted[i])return;e.emitted[i]=!0,Xe(c,e,6,o)}}function jl(e,t,n=!1){const r=t.emitsCache,o=r.get(e);if(o!==void 0)return o;const s=e.emits;let l={},i=!1;if(!oe(e)){const a=c=>{const u=jl(c,t,!0);u&&(i=!0,Ae(l,u))};!n&&t.mixins.length&&t.mixins.forEach(a),e.extends&&a(e.extends),e.mixins&&e.mixins.forEach(a)}return!s&&!i?(ke(e)&&r.set(e,null),null):(Q(s)?s.forEach(a=>l[a]=null):Ae(l,s),ke(e)&&r.set(e,l),l)}function Ar(e,t){return!e||!Fn(t)?!1:(t=t.slice(2).replace(/Once$/,""),de(e,t[0].toLowerCase()+t.slice(1))||de(e,Yt(t))||de(e,t))}let Me=null,Bl=null;function hr(e){const t=Me;return Me=e,Bl=e&&e.type.__scopeId||null,t}function De(e,t=Me,n){if(!t||e._n)return e;const r=(...o)=>{r._d&&vs(-1);const s=hr(t);let l;try{l=e(...o)}finally{hr(s),r._d&&vs(1)}return l};return r._n=!0,r._c=!0,r._d=!0,r}function Fr(e){const{type:t,vnode:n,proxy:r,withProxy:o,props:s,propsOptions:[l],slots:i,attrs:a,emit:c,render:u,renderCache:f,data:h,setupState:g,ctx:y,inheritAttrs:w}=e;let C,v;const b=hr(e);try{if(n.shapeFlag&4){const S=o||r;C=tt(u.call(S,S,f,s,g,h,y)),v=a}else{const S=t;C=tt(S.length>1?S(s,{attrs:a,slots:i,emit:c}):S(s,null)),v=t.props?a:sc(a)}}catch(S){Sn.length=0,zn(S,e,1),C=te(Ye)}let P=C;if(v&&w!==!1){const S=Object.keys(v),{shapeFlag:U}=P;S.length&&U&7&&(l&&S.some(go)&&(v=lc(v,l)),P=$t(P,v))}return n.dirs&&(P=$t(P),P.dirs=P.dirs?P.dirs.concat(n.dirs):n.dirs),n.transition&&(P.transition=n.transition),C=P,hr(b),C}const sc=e=>{let t;for(const n in e)(n==="class"||n==="style"||Fn(n))&&((t||(t={}))[n]=e[n]);return t},lc=(e,t)=>{const n={};for(const r in e)(!go(r)||!(r.slice(9)in t))&&(n[r]=e[r]);return n};function ic(e,t,n){const{props:r,children:o,component:s}=e,{props:l,children:i,patchFlag:a}=t,c=s.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&a>=0){if(a&1024)return!0;if(a&16)return r?ls(r,l,c):!!l;if(a&8){const u=t.dynamicProps;for(let f=0;fe.__isSuspense;function zl(e,t){t&&t.pendingBranch?Q(e)?t.effects.push(...e):t.effects.push(e):nc(e)}function uc(e,t){return Ao(e,null,t)}const Xn={};function st(e,t,n){return Ao(e,t,n)}function Ao(e,t,{immediate:n,deep:r,flush:o,onTrack:s,onTrigger:l}=xe){var i;const a=yl()===((i=Oe)==null?void 0:i.scope)?Oe:null;let c,u=!1,f=!1;if(Ie(e)?(c=()=>e.value,u=fr(e)):sn(e)?(c=()=>e,r=!0):Q(e)?(f=!0,u=e.some(S=>sn(S)||fr(S)),c=()=>e.map(S=>{if(Ie(S))return S.value;if(sn(S))return Ut(S);if(oe(S))return Ot(S,a,2)})):oe(e)?t?c=()=>Ot(e,a,2):c=()=>{if(!(a&&a.isUnmounted))return h&&h(),Xe(e,a,3,[g])}:c=ot,t&&r){const S=c;c=()=>Ut(S())}let h,g=S=>{h=b.onStop=()=>{Ot(S,a,4)}},y;if(dn)if(g=ot,t?n&&Xe(t,a,3,[c(),f?[]:void 0,g]):c(),o==="sync"){const S=su();y=S.__watcherHandles||(S.__watcherHandles=[])}else return ot;let w=f?new Array(e.length).fill(Xn):Xn;const C=()=>{if(b.active)if(t){const S=b.run();(r||u||(f?S.some((U,Z)=>On(U,w[Z])):On(S,w)))&&(h&&h(),Xe(t,a,3,[S,w===Xn?void 0:f&&w[0]===Xn?[]:w,g]),w=S)}else b.run()};C.allowRecurse=!!t;let v;o==="sync"?v=C:o==="post"?v=()=>Ue(C,a&&a.suspense):(C.pre=!0,a&&(C.id=a.uid),v=()=>Pr(C));const b=new Eo(c,v);t?n?C():w=b.run():o==="post"?Ue(b.run.bind(b),a&&a.suspense):b.run();const P=()=>{b.stop(),a&&a.scope&&vo(a.scope.effects,b)};return y&&y.push(P),P}function fc(e,t,n){const r=this.proxy,o=ge(e)?e.includes(".")?Vl(r,e):()=>r[e]:e.bind(r,r);let s;oe(t)?s=t:(s=t.handler,n=t);const l=Oe;fn(this);const i=Ao(o,s.bind(r),n);return l?fn(l):Kt(),i}function Vl(e,t){const n=t.split(".");return()=>{let r=e;for(let o=0;o{Ut(n,t)});else if(vl(e))for(const n in e)Ut(e[n],t);return e}function pr(e,t){const n=Me;if(n===null)return e;const r=$r(n)||n.proxy,o=e.dirs||(e.dirs=[]);for(let s=0;s{e.isMounted=!0}),Un(()=>{e.isUnmounting=!0}),e}const Qe=[Function,Array],Ul={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:Qe,onEnter:Qe,onAfterEnter:Qe,onEnterCancelled:Qe,onBeforeLeave:Qe,onLeave:Qe,onAfterLeave:Qe,onLeaveCancelled:Qe,onBeforeAppear:Qe,onAppear:Qe,onAfterAppear:Qe,onAppearCancelled:Qe},hc={name:"BaseTransition",props:Ul,setup(e,{slots:t}){const n=li(),r=dc();let o;return()=>{const s=t.default&&Wl(t.default(),!0);if(!s||!s.length)return;let l=s[0];if(s.length>1){for(const w of s)if(w.type!==Ye){l=w;break}}const i=pe(e),{mode:a}=i;if(r.isLeaving)return jr(l);const c=is(l);if(!c)return jr(l);const u=ro(c,i,r,n);oo(c,u);const f=n.subTree,h=f&&is(f);let g=!1;const{getTransitionKey:y}=c.type;if(y){const w=y();o===void 0?o=w:w!==o&&(o=w,g=!0)}if(h&&h.type!==Ye&&(!zt(c,h)||g)){const w=ro(h,i,r,n);if(oo(h,w),a==="out-in")return r.isLeaving=!0,w.afterLeave=()=>{r.isLeaving=!1,n.update.active!==!1&&n.update()},jr(l);a==="in-out"&&c.type!==Ye&&(w.delayLeave=(C,v,b)=>{const P=ql(r,h);P[String(h.key)]=h,C._leaveCb=()=>{v(),C._leaveCb=void 0,delete u.delayedLeave},u.delayedLeave=b})}return l}}},pc=hc;function ql(e,t){const{leavingVNodes:n}=e;let r=n.get(t.type);return r||(r=Object.create(null),n.set(t.type,r)),r}function ro(e,t,n,r){const{appear:o,mode:s,persisted:l=!1,onBeforeEnter:i,onEnter:a,onAfterEnter:c,onEnterCancelled:u,onBeforeLeave:f,onLeave:h,onAfterLeave:g,onLeaveCancelled:y,onBeforeAppear:w,onAppear:C,onAfterAppear:v,onAppearCancelled:b}=t,P=String(e.key),S=ql(n,e),U=(m,z)=>{m&&Xe(m,r,9,z)},Z=(m,z)=>{const M=z[1];U(m,z),Q(m)?m.every(G=>G.length<=1)&&M():m.length<=1&&M()},$={mode:s,persisted:l,beforeEnter(m){let z=i;if(!n.isMounted)if(o)z=w||i;else return;m._leaveCb&&m._leaveCb(!0);const M=S[P];M&&zt(e,M)&&M.el._leaveCb&&M.el._leaveCb(),U(z,[m])},enter(m){let z=a,M=c,G=u;if(!n.isMounted)if(o)z=C||a,M=v||c,G=b||u;else return;let L=!1;const R=m._enterCb=I=>{L||(L=!0,I?U(G,[m]):U(M,[m]),$.delayedLeave&&$.delayedLeave(),m._enterCb=void 0)};z?Z(z,[m,R]):R()},leave(m,z){const M=String(e.key);if(m._enterCb&&m._enterCb(!0),n.isUnmounting)return z();U(f,[m]);let G=!1;const L=m._leaveCb=R=>{G||(G=!0,z(),R?U(y,[m]):U(g,[m]),m._leaveCb=void 0,S[M]===e&&delete S[M])};S[M]=e,h?Z(h,[m,L]):L()},clone(m){return ro(m,t,n,r)}};return $}function jr(e){if(Vn(e))return e=$t(e),e.children=null,e}function is(e){return Vn(e)?e.children?e.children[0]:void 0:e}function oo(e,t){e.shapeFlag&6&&e.component?oo(e.component.subTree,t):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function Wl(e,t=!1,n){let r=[],o=0;for(let s=0;s1)for(let s=0;sAe({name:e.name},t,{setup:e}))():e}const an=e=>!!e.type.__asyncLoader;function ve(e){oe(e)&&(e={loader:e});const{loader:t,loadingComponent:n,errorComponent:r,delay:o=200,timeout:s,suspensible:l=!0,onError:i}=e;let a=null,c,u=0;const f=()=>(u++,a=null,h()),h=()=>{let g;return a||(g=a=t().catch(y=>{if(y=y instanceof Error?y:new Error(String(y)),i)return new Promise((w,C)=>{i(y,()=>w(f()),()=>C(y),u+1)});throw y}).then(y=>g!==a&&a?a:(y&&(y.__esModule||y[Symbol.toStringTag]==="Module")&&(y=y.default),c=y,y)))};return ue({name:"AsyncComponentWrapper",__asyncLoader:h,get __asyncResolved(){return c},setup(){const g=Oe;if(c)return()=>Br(c,g);const y=b=>{a=null,zn(b,g,13,!r)};if(l&&g.suspense||dn)return h().then(b=>()=>Br(b,g)).catch(b=>(y(b),()=>r?te(r,{error:b}):null));const w=me(!1),C=me(),v=me(!!o);return o&&setTimeout(()=>{v.value=!1},o),s!=null&&setTimeout(()=>{if(!w.value&&!C.value){const b=new Error(`Async component timed out after ${s}ms.`);y(b),C.value=b}},s),h().then(()=>{w.value=!0,g.parent&&Vn(g.parent.vnode)&&Pr(g.parent.update)}).catch(b=>{y(b),C.value=b}),()=>{if(w.value&&c)return Br(c,g);if(C.value&&r)return te(r,{error:C.value});if(n&&!v.value)return te(n)}}})}function Br(e,t){const{ref:n,props:r,children:o,ce:s}=t.vnode,l=te(e,r,o);return l.ref=n,l.ce=s,delete t.vnode.ce,l}const Vn=e=>e.type.__isKeepAlive;function mc(e,t){Kl(e,"a",t)}function gc(e,t){Kl(e,"da",t)}function Kl(e,t,n=Oe){const r=e.__wdc||(e.__wdc=()=>{let o=n;for(;o;){if(o.isDeactivated)return;o=o.parent}return e()});if(Or(t,r,n),n){let o=n.parent;for(;o&&o.parent;)Vn(o.parent.vnode)&&vc(r,t,n,o),o=o.parent}}function vc(e,t,n,r){const o=Or(t,e,r,!0);Rr(()=>{vo(r[t],o)},n)}function Or(e,t,n=Oe,r=!1){if(n){const o=n[e]||(n[e]=[]),s=t.__weh||(t.__weh=(...l)=>{if(n.isUnmounted)return;mn(),fn(n);const i=Xe(t,n,e,l);return Kt(),gn(),i});return r?o.unshift(s):o.push(s),s}}const yt=e=>(t,n=Oe)=>(!dn||e==="sp")&&Or(e,(...r)=>t(...r),n),_c=yt("bm"),Je=yt("m"),bc=yt("bu"),yc=yt("u"),Un=yt("bum"),Rr=yt("um"),Ec=yt("sp"),wc=yt("rtg"),kc=yt("rtc");function xc(e,t=Oe){Or("ec",e,t)}const Gl="components";function bt(e,t){return Cc(Gl,e,!0,t)||e}const Lc=Symbol.for("v-ndc");function Cc(e,t,n=!0,r=!1){const o=Me||Oe;if(o){const s=o.type;if(e===Gl){const i=nu(s,!1);if(i&&(i===t||i===ft(t)||i===Lr(ft(t))))return s}const l=as(o[e]||s[e],t)||as(o.appContext[e],t);return!l&&r?s:l}}function as(e,t){return e&&(e[t]||e[ft(t)]||e[Lr(ft(t))])}function It(e,t,n,r){let o;const s=n&&n[r];if(Q(e)||ge(e)){o=new Array(e.length);for(let l=0,i=e.length;lt(l,i,void 0,s&&s[i]));else{const l=Object.keys(e);o=new Array(l.length);for(let i=0,a=l.length;i_r(t)?!(t.type===Ye||t.type===we&&!Yl(t.children)):!0)?e:null}const so=e=>e?ii(e)?$r(e)||e.proxy:so(e.parent):null,Cn=Ae(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>so(e.parent),$root:e=>so(e.root),$emit:e=>e.emit,$options:e=>Oo(e),$forceUpdate:e=>e.f||(e.f=()=>Pr(e.update)),$nextTick:e=>e.n||(e.n=Sr.bind(e.proxy)),$watch:e=>fc.bind(e)}),zr=(e,t)=>e!==xe&&!e.__isScriptSetup&&de(e,t),Tc={get({_:e},t){const{ctx:n,setupState:r,data:o,props:s,accessCache:l,type:i,appContext:a}=e;let c;if(t[0]!=="$"){const g=l[t];if(g!==void 0)switch(g){case 1:return r[t];case 2:return o[t];case 4:return n[t];case 3:return s[t]}else{if(zr(r,t))return l[t]=1,r[t];if(o!==xe&&de(o,t))return l[t]=2,o[t];if((c=e.propsOptions[0])&&de(c,t))return l[t]=3,s[t];if(n!==xe&&de(n,t))return l[t]=4,n[t];lo&&(l[t]=0)}}const u=Cn[t];let f,h;if(u)return t==="$attrs"&&We(e,"get",t),u(e);if((f=i.__cssModules)&&(f=f[t]))return f;if(n!==xe&&de(n,t))return l[t]=4,n[t];if(h=a.config.globalProperties,de(h,t))return h[t]},set({_:e},t,n){const{data:r,setupState:o,ctx:s}=e;return zr(o,t)?(o[t]=n,!0):r!==xe&&de(r,t)?(r[t]=n,!0):de(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(s[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:r,appContext:o,propsOptions:s}},l){let i;return!!n[l]||e!==xe&&de(e,l)||zr(t,l)||(i=s[0])&&de(i,l)||de(r,l)||de(Cn,l)||de(o.config.globalProperties,l)},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:de(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function cs(e){return Q(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let lo=!0;function Sc(e){const t=Oo(e),n=e.proxy,r=e.ctx;lo=!1,t.beforeCreate&&us(t.beforeCreate,e,"bc");const{data:o,computed:s,methods:l,watch:i,provide:a,inject:c,created:u,beforeMount:f,mounted:h,beforeUpdate:g,updated:y,activated:w,deactivated:C,beforeDestroy:v,beforeUnmount:b,destroyed:P,unmounted:S,render:U,renderTracked:Z,renderTriggered:$,errorCaptured:m,serverPrefetch:z,expose:M,inheritAttrs:G,components:L,directives:R,filters:I}=t;if(c&&Pc(c,r,null),l)for(const re in l){const se=l[re];oe(se)&&(r[re]=se.bind(n))}if(o){const re=o.call(n,n);ke(re)&&(e.data=vn(re))}if(lo=!0,s)for(const re in s){const se=s[re],He=oe(se)?se.bind(n,n):oe(se.get)?se.get.bind(n,n):ot,Ne=!oe(se)&&oe(se.set)?se.set.bind(n):ot,Ve=F({get:He,set:Ne});Object.defineProperty(r,re,{enumerable:!0,configurable:!0,get:()=>Ve.value,set:Fe=>Ve.value=Fe})}if(i)for(const re in i)Jl(i[re],r,n,re);if(a){const re=oe(a)?a.call(n):a;Reflect.ownKeys(re).forEach(se=>{Wt(se,re[se])})}u&&us(u,e,"c");function V(re,se){Q(se)?se.forEach(He=>re(He.bind(n))):se&&re(se.bind(n))}if(V(_c,f),V(Je,h),V(bc,g),V(yc,y),V(mc,w),V(gc,C),V(xc,m),V(kc,Z),V(wc,$),V(Un,b),V(Rr,S),V(Ec,z),Q(M))if(M.length){const re=e.exposed||(e.exposed={});M.forEach(se=>{Object.defineProperty(re,se,{get:()=>n[se],set:He=>n[se]=He})})}else e.exposed||(e.exposed={});U&&e.render===ot&&(e.render=U),G!=null&&(e.inheritAttrs=G),L&&(e.components=L),R&&(e.directives=R)}function Pc(e,t,n=ot){Q(e)&&(e=io(e));for(const r in e){const o=e[r];let s;ke(o)?"default"in o?s=Pe(o.from||r,o.default,!0):s=Pe(o.from||r):s=Pe(o),Ie(s)?Object.defineProperty(t,r,{enumerable:!0,configurable:!0,get:()=>s.value,set:l=>s.value=l}):t[r]=s}}function us(e,t,n){Xe(Q(e)?e.map(r=>r.bind(t.proxy)):e.bind(t.proxy),t,n)}function Jl(e,t,n,r){const o=r.includes(".")?Vl(n,r):()=>n[r];if(ge(e)){const s=t[e];oe(s)&&st(o,s)}else if(oe(e))st(o,e.bind(n));else if(ke(e))if(Q(e))e.forEach(s=>Jl(s,t,n,r));else{const s=oe(e.handler)?e.handler.bind(n):t[e.handler];oe(s)&&st(o,s,e)}}function Oo(e){const t=e.type,{mixins:n,extends:r}=t,{mixins:o,optionsCache:s,config:{optionMergeStrategies:l}}=e.appContext,i=s.get(t);let a;return i?a=i:!o.length&&!n&&!r?a=t:(a={},o.length&&o.forEach(c=>mr(a,c,l,!0)),mr(a,t,l)),ke(t)&&s.set(t,a),a}function mr(e,t,n,r=!1){const{mixins:o,extends:s}=t;s&&mr(e,s,n,!0),o&&o.forEach(l=>mr(e,l,n,!0));for(const l in t)if(!(r&&l==="expose")){const i=Ac[l]||n&&n[l];e[l]=i?i(e[l],t[l]):t[l]}return e}const Ac={data:fs,props:ds,emits:ds,methods:xn,computed:xn,beforeCreate:Be,created:Be,beforeMount:Be,mounted:Be,beforeUpdate:Be,updated:Be,beforeDestroy:Be,beforeUnmount:Be,destroyed:Be,unmounted:Be,activated:Be,deactivated:Be,errorCaptured:Be,serverPrefetch:Be,components:xn,directives:xn,watch:Rc,provide:fs,inject:Oc};function fs(e,t){return t?e?function(){return Ae(oe(e)?e.call(this,this):e,oe(t)?t.call(this,this):t)}:t:e}function Oc(e,t){return xn(io(e),io(t))}function io(e){if(Q(e)){const t={};for(let n=0;n1)return n&&oe(t)?t.call(r&&r.proxy):t}}function Nc(e,t,n,r=!1){const o={},s={};cr(s,Ir,1),e.propsDefaults=Object.create(null),Zl(e,t,o,s);for(const l in e.propsOptions[0])l in o||(o[l]=void 0);n?e.props=r?o:Va(o):e.type.props?e.props=o:e.props=s,e.attrs=s}function Dc(e,t,n,r){const{props:o,attrs:s,vnode:{patchFlag:l}}=e,i=pe(o),[a]=e.propsOptions;let c=!1;if((r||l>0)&&!(l&16)){if(l&8){const u=e.vnode.dynamicProps;for(let f=0;f{a=!0;const[h,g]=Xl(f,t,!0);Ae(l,h),g&&i.push(...g)};!n&&t.mixins.length&&t.mixins.forEach(u),e.extends&&u(e.extends),e.mixins&&e.mixins.forEach(u)}if(!s&&!a)return ke(e)&&r.set(e,rn),rn;if(Q(s))for(let u=0;u-1,g[1]=w<0||y-1||de(g,"default"))&&i.push(f)}}}const c=[l,i];return ke(e)&&r.set(e,c),c}function hs(e){return e[0]!=="$"}function ps(e){const t=e&&e.toString().match(/^\s*(function|class) (\w+)/);return t?t[2]:e===null?"null":""}function ms(e,t){return ps(e)===ps(t)}function gs(e,t){return Q(t)?t.findIndex(n=>ms(n,e)):oe(t)&&ms(t,e)?0:-1}const ei=e=>e[0]==="_"||e==="$stable",Ro=e=>Q(e)?e.map(tt):[tt(e)],Mc=(e,t,n)=>{if(t._n)return t;const r=De((...o)=>Ro(t(...o)),n);return r._c=!1,r},ti=(e,t,n)=>{const r=e._ctx;for(const o in e){if(ei(o))continue;const s=e[o];if(oe(s))t[o]=Mc(o,s,r);else if(s!=null){const l=Ro(s);t[o]=()=>l}}},ni=(e,t)=>{const n=Ro(t);e.slots.default=()=>n},Hc=(e,t)=>{if(e.vnode.shapeFlag&32){const n=t._;n?(e.slots=pe(t),cr(t,"_",n)):ti(t,e.slots={})}else e.slots={},t&&ni(e,t);cr(e.slots,Ir,1)},Fc=(e,t,n)=>{const{vnode:r,slots:o}=e;let s=!0,l=xe;if(r.shapeFlag&32){const i=t._;i?n&&i===1?s=!1:(Ae(o,t),!n&&i===1&&delete o._):(s=!t.$stable,ti(t,o)),l=t}else t&&(ni(e,t),l={default:1});if(s)for(const i in o)!ei(i)&&!(i in l)&&delete o[i]};function vr(e,t,n,r,o=!1){if(Q(e)){e.forEach((h,g)=>vr(h,t&&(Q(t)?t[g]:t),n,r,o));return}if(an(r)&&!o)return;const s=r.shapeFlag&4?$r(r.component)||r.component.proxy:r.el,l=o?null:s,{i,r:a}=e,c=t&&t.r,u=i.refs===xe?i.refs={}:i.refs,f=i.setupState;if(c!=null&&c!==a&&(ge(c)?(u[c]=null,de(f,c)&&(f[c]=null)):Ie(c)&&(c.value=null)),oe(a))Ot(a,i,12,[l,u]);else{const h=ge(a),g=Ie(a);if(h||g){const y=()=>{if(e.f){const w=h?de(f,a)?f[a]:u[a]:a.value;o?Q(w)&&vo(w,s):Q(w)?w.includes(s)||w.push(s):h?(u[a]=[s],de(f,a)&&(f[a]=u[a])):(a.value=[s],e.k&&(u[e.k]=a.value))}else h?(u[a]=l,de(f,a)&&(f[a]=l)):g&&(a.value=l,e.k&&(u[e.k]=l))};l?(y.id=-1,Ue(y,n)):y()}}}let xt=!1;const er=e=>/svg/.test(e.namespaceURI)&&e.tagName!=="foreignObject",tr=e=>e.nodeType===8;function jc(e){const{mt:t,p:n,o:{patchProp:r,createText:o,nextSibling:s,parentNode:l,remove:i,insert:a,createComment:c}}=e,u=(v,b)=>{if(!b.hasChildNodes()){n(null,v,b),dr(),b._vnode=v;return}xt=!1,f(b.firstChild,v,null,null,null),dr(),b._vnode=v,xt&&console.error("Hydration completed but contains mismatches.")},f=(v,b,P,S,U,Z=!1)=>{const $=tr(v)&&v.data==="[",m=()=>w(v,b,P,S,U,$),{type:z,ref:M,shapeFlag:G,patchFlag:L}=b;let R=v.nodeType;b.el=v,L===-2&&(Z=!1,b.dynamicChildren=null);let I=null;switch(z){case un:R!==3?b.children===""?(a(b.el=o(""),l(v),v),I=v):I=m():(v.data!==b.children&&(xt=!0,v.data=b.children),I=s(v));break;case Ye:R!==8||$?I=m():I=s(v);break;case Tn:if($&&(v=s(v),R=v.nodeType),R===1||R===3){I=v;const le=!b.children.length;for(let V=0;V{Z=Z||!!b.dynamicChildren;const{type:$,props:m,patchFlag:z,shapeFlag:M,dirs:G}=b,L=$==="input"&&G||$==="option";if(L||z!==-1){if(G&&ct(b,null,P,"created"),m)if(L||!Z||z&48)for(const I in m)(L&&I.endsWith("value")||Fn(I)&&!Ln(I))&&r(v,I,null,m[I],!1,void 0,P);else m.onClick&&r(v,"onClick",null,m.onClick,!1,void 0,P);let R;if((R=m&&m.onVnodeBeforeMount)&&Ze(R,P,b),G&&ct(b,null,P,"beforeMount"),((R=m&&m.onVnodeMounted)||G)&&zl(()=>{R&&Ze(R,P,b),G&&ct(b,null,P,"mounted")},S),M&16&&!(m&&(m.innerHTML||m.textContent))){let I=g(v.firstChild,b,v,P,S,U,Z);for(;I;){xt=!0;const le=I;I=I.nextSibling,i(le)}}else M&8&&v.textContent!==b.children&&(xt=!0,v.textContent=b.children)}return v.nextSibling},g=(v,b,P,S,U,Z,$)=>{$=$||!!b.dynamicChildren;const m=b.children,z=m.length;for(let M=0;M{const{slotScopeIds:$}=b;$&&(U=U?U.concat($):$);const m=l(v),z=g(s(v),b,m,P,S,U,Z);return z&&tr(z)&&z.data==="]"?s(b.anchor=z):(xt=!0,a(b.anchor=c("]"),m,z),z)},w=(v,b,P,S,U,Z)=>{if(xt=!0,b.el=null,Z){const z=C(v);for(;;){const M=s(v);if(M&&M!==z)i(M);else break}}const $=s(v),m=l(v);return i(v),n(null,b,m,$,P,S,er(m),U),$},C=v=>{let b=0;for(;v;)if(v=s(v),v&&tr(v)&&(v.data==="["&&b++,v.data==="]")){if(b===0)return s(v);b--}return v};return[u,f]}const Ue=zl;function Bc(e){return zc(e,jc)}function zc(e,t){const n=Zr();n.__VUE__=!0;const{insert:r,remove:o,patchProp:s,createElement:l,createText:i,createComment:a,setText:c,setElementText:u,parentNode:f,nextSibling:h,setScopeId:g=ot,insertStaticContent:y}=e,w=(d,p,_,E=null,x=null,T=null,H=!1,O=null,D=!!p.dynamicChildren)=>{if(d===p)return;d&&!zt(d,p)&&(E=k(d),Fe(d,x,T,!0),d=null),p.patchFlag===-2&&(D=!1,p.dynamicChildren=null);const{type:A,ref:Y,shapeFlag:q}=p;switch(A){case un:C(d,p,_,E);break;case Ye:v(d,p,_,E);break;case Tn:d==null&&b(p,_,E,H);break;case we:L(d,p,_,E,x,T,H,O,D);break;default:q&1?U(d,p,_,E,x,T,H,O,D):q&6?R(d,p,_,E,x,T,H,O,D):(q&64||q&128)&&A.process(d,p,_,E,x,T,H,O,D,N)}Y!=null&&x&&vr(Y,d&&d.ref,T,p||d,!p)},C=(d,p,_,E)=>{if(d==null)r(p.el=i(p.children),_,E);else{const x=p.el=d.el;p.children!==d.children&&c(x,p.children)}},v=(d,p,_,E)=>{d==null?r(p.el=a(p.children||""),_,E):p.el=d.el},b=(d,p,_,E)=>{[d.el,d.anchor]=y(d.children,p,_,E,d.el,d.anchor)},P=({el:d,anchor:p},_,E)=>{let x;for(;d&&d!==p;)x=h(d),r(d,_,E),d=x;r(p,_,E)},S=({el:d,anchor:p})=>{let _;for(;d&&d!==p;)_=h(d),o(d),d=_;o(p)},U=(d,p,_,E,x,T,H,O,D)=>{H=H||p.type==="svg",d==null?Z(p,_,E,x,T,H,O,D):z(d,p,x,T,H,O,D)},Z=(d,p,_,E,x,T,H,O)=>{let D,A;const{type:Y,props:q,shapeFlag:J,transition:ne,dirs:ie}=d;if(D=d.el=l(d.type,T,q&&q.is,q),J&8?u(D,d.children):J&16&&m(d.children,D,null,E,x,T&&Y!=="foreignObject",H,O),ie&&ct(d,null,E,"created"),$(D,d,d.scopeId,H,E),q){for(const be in q)be!=="value"&&!Ln(be)&&s(D,be,null,q[be],T,d.children,E,x,$e);"value"in q&&s(D,"value",null,q.value),(A=q.onVnodeBeforeMount)&&Ze(A,E,d)}ie&&ct(d,null,E,"beforeMount");const ye=(!x||x&&!x.pendingBranch)&&ne&&!ne.persisted;ye&&ne.beforeEnter(D),r(D,p,_),((A=q&&q.onVnodeMounted)||ye||ie)&&Ue(()=>{A&&Ze(A,E,d),ye&&ne.enter(D),ie&&ct(d,null,E,"mounted")},x)},$=(d,p,_,E,x)=>{if(_&&g(d,_),E)for(let T=0;T{for(let A=D;A{const O=p.el=d.el;let{patchFlag:D,dynamicChildren:A,dirs:Y}=p;D|=d.patchFlag&16;const q=d.props||xe,J=p.props||xe;let ne;_&&Mt(_,!1),(ne=J.onVnodeBeforeUpdate)&&Ze(ne,_,p,d),Y&&ct(p,d,_,"beforeUpdate"),_&&Mt(_,!0);const ie=x&&p.type!=="foreignObject";if(A?M(d.dynamicChildren,A,O,_,E,ie,T):H||se(d,p,O,null,_,E,ie,T,!1),D>0){if(D&16)G(O,p,q,J,_,E,x);else if(D&2&&q.class!==J.class&&s(O,"class",null,J.class,x),D&4&&s(O,"style",q.style,J.style,x),D&8){const ye=p.dynamicProps;for(let be=0;be{ne&&Ze(ne,_,p,d),Y&&ct(p,d,_,"updated")},E)},M=(d,p,_,E,x,T,H)=>{for(let O=0;O{if(_!==E){if(_!==xe)for(const O in _)!Ln(O)&&!(O in E)&&s(d,O,_[O],null,H,p.children,x,T,$e);for(const O in E){if(Ln(O))continue;const D=E[O],A=_[O];D!==A&&O!=="value"&&s(d,O,A,D,H,p.children,x,T,$e)}"value"in E&&s(d,"value",_.value,E.value)}},L=(d,p,_,E,x,T,H,O,D)=>{const A=p.el=d?d.el:i(""),Y=p.anchor=d?d.anchor:i("");let{patchFlag:q,dynamicChildren:J,slotScopeIds:ne}=p;ne&&(O=O?O.concat(ne):ne),d==null?(r(A,_,E),r(Y,_,E),m(p.children,_,Y,x,T,H,O,D)):q>0&&q&64&&J&&d.dynamicChildren?(M(d.dynamicChildren,J,_,x,T,H,O),(p.key!=null||x&&p===x.subTree)&&ri(d,p,!0)):se(d,p,_,Y,x,T,H,O,D)},R=(d,p,_,E,x,T,H,O,D)=>{p.slotScopeIds=O,d==null?p.shapeFlag&512?x.ctx.activate(p,_,E,H,D):I(p,_,E,x,T,H,D):le(d,p,D)},I=(d,p,_,E,x,T,H)=>{const O=d.component=Qc(d,E,x);if(Vn(d)&&(O.ctx.renderer=N),Zc(O),O.asyncDep){if(x&&x.registerDep(O,V),!d.el){const D=O.subTree=te(Ye);v(null,D,p,_)}return}V(O,d,p,_,x,T,H)},le=(d,p,_)=>{const E=p.component=d.component;if(ic(d,p,_))if(E.asyncDep&&!E.asyncResolved){re(E,p,_);return}else E.next=p,tc(E.update),E.update();else p.el=d.el,E.vnode=p},V=(d,p,_,E,x,T,H)=>{const O=()=>{if(d.isMounted){let{next:Y,bu:q,u:J,parent:ne,vnode:ie}=d,ye=Y,be;Mt(d,!1),Y?(Y.el=ie.el,re(d,Y,H)):Y=ie,q&&Hr(q),(be=Y.props&&Y.props.onVnodeBeforeUpdate)&&Ze(be,ne,Y,ie),Mt(d,!0);const Te=Fr(d),et=d.subTree;d.subTree=Te,w(et,Te,f(et.el),k(et),d,x,T),Y.el=Te.el,ye===null&&ac(d,Te.el),J&&Ue(J,x),(be=Y.props&&Y.props.onVnodeUpdated)&&Ue(()=>Ze(be,ne,Y,ie),x)}else{let Y;const{el:q,props:J}=p,{bm:ne,m:ie,parent:ye}=d,be=an(p);if(Mt(d,!1),ne&&Hr(ne),!be&&(Y=J&&J.onVnodeBeforeMount)&&Ze(Y,ye,p),Mt(d,!0),q&&ce){const Te=()=>{d.subTree=Fr(d),ce(q,d.subTree,d,x,null)};be?p.type.__asyncLoader().then(()=>!d.isUnmounted&&Te()):Te()}else{const Te=d.subTree=Fr(d);w(null,Te,_,E,d,x,T),p.el=Te.el}if(ie&&Ue(ie,x),!be&&(Y=J&&J.onVnodeMounted)){const Te=p;Ue(()=>Ze(Y,ye,Te),x)}(p.shapeFlag&256||ye&&an(ye.vnode)&&ye.vnode.shapeFlag&256)&&d.a&&Ue(d.a,x),d.isMounted=!0,p=_=E=null}},D=d.effect=new Eo(O,()=>Pr(A),d.scope),A=d.update=()=>D.run();A.id=d.uid,Mt(d,!0),A()},re=(d,p,_)=>{p.component=d;const E=d.vnode.props;d.vnode=p,d.next=null,Dc(d,p.props,E,_),Fc(d,p.children,_),mn(),ss(),gn()},se=(d,p,_,E,x,T,H,O,D=!1)=>{const A=d&&d.children,Y=d?d.shapeFlag:0,q=p.children,{patchFlag:J,shapeFlag:ne}=p;if(J>0){if(J&128){Ne(A,q,_,E,x,T,H,O,D);return}else if(J&256){He(A,q,_,E,x,T,H,O,D);return}}ne&8?(Y&16&&$e(A,x,T),q!==A&&u(_,q)):Y&16?ne&16?Ne(A,q,_,E,x,T,H,O,D):$e(A,x,T,!0):(Y&8&&u(_,""),ne&16&&m(q,_,E,x,T,H,O,D))},He=(d,p,_,E,x,T,H,O,D)=>{d=d||rn,p=p||rn;const A=d.length,Y=p.length,q=Math.min(A,Y);let J;for(J=0;JY?$e(d,x,T,!0,!1,q):m(p,_,E,x,T,H,O,D,q)},Ne=(d,p,_,E,x,T,H,O,D)=>{let A=0;const Y=p.length;let q=d.length-1,J=Y-1;for(;A<=q&&A<=J;){const ne=d[A],ie=p[A]=D?Tt(p[A]):tt(p[A]);if(zt(ne,ie))w(ne,ie,_,null,x,T,H,O,D);else break;A++}for(;A<=q&&A<=J;){const ne=d[q],ie=p[J]=D?Tt(p[J]):tt(p[J]);if(zt(ne,ie))w(ne,ie,_,null,x,T,H,O,D);else break;q--,J--}if(A>q){if(A<=J){const ne=J+1,ie=neJ)for(;A<=q;)Fe(d[A],x,T,!0),A++;else{const ne=A,ie=A,ye=new Map;for(A=ie;A<=J;A++){const Ke=p[A]=D?Tt(p[A]):tt(p[A]);Ke.key!=null&&ye.set(Ke.key,A)}let be,Te=0;const et=J-ie+1;let Zt=!1,Ko=0;const _n=new Array(et);for(A=0;A=et){Fe(Ke,x,T,!0);continue}let at;if(Ke.key!=null)at=ye.get(Ke.key);else for(be=ie;be<=J;be++)if(_n[be-ie]===0&&zt(Ke,p[be])){at=be;break}at===void 0?Fe(Ke,x,T,!0):(_n[at-ie]=A+1,at>=Ko?Ko=at:Zt=!0,w(Ke,p[at],_,null,x,T,H,O,D),Te++)}const Go=Zt?Vc(_n):rn;for(be=Go.length-1,A=et-1;A>=0;A--){const Ke=ie+A,at=p[Ke],Yo=Ke+1{const{el:T,type:H,transition:O,children:D,shapeFlag:A}=d;if(A&6){Ve(d.component.subTree,p,_,E);return}if(A&128){d.suspense.move(p,_,E);return}if(A&64){H.move(d,p,_,N);return}if(H===we){r(T,p,_);for(let q=0;qO.enter(T),x);else{const{leave:q,delayLeave:J,afterLeave:ne}=O,ie=()=>r(T,p,_),ye=()=>{q(T,()=>{ie(),ne&&ne()})};J?J(T,ie,ye):ye()}else r(T,p,_)},Fe=(d,p,_,E=!1,x=!1)=>{const{type:T,props:H,ref:O,children:D,dynamicChildren:A,shapeFlag:Y,patchFlag:q,dirs:J}=d;if(O!=null&&vr(O,null,_,d,!0),Y&256){p.ctx.deactivate(d);return}const ne=Y&1&&J,ie=!an(d);let ye;if(ie&&(ye=H&&H.onVnodeBeforeUnmount)&&Ze(ye,p,d),Y&6)it(d.component,_,E);else{if(Y&128){d.suspense.unmount(_,E);return}ne&&ct(d,null,p,"beforeUnmount"),Y&64?d.type.remove(d,p,_,x,N,E):A&&(T!==we||q>0&&q&64)?$e(A,p,_,!1,!0):(T===we&&q&384||!x&&Y&16)&&$e(D,p,_),E&&Et(d)}(ie&&(ye=H&&H.onVnodeUnmounted)||ne)&&Ue(()=>{ye&&Ze(ye,p,d),ne&&ct(d,null,p,"unmounted")},_)},Et=d=>{const{type:p,el:_,anchor:E,transition:x}=d;if(p===we){wt(_,E);return}if(p===Tn){S(d);return}const T=()=>{o(_),x&&!x.persisted&&x.afterLeave&&x.afterLeave()};if(d.shapeFlag&1&&x&&!x.persisted){const{leave:H,delayLeave:O}=x,D=()=>H(_,T);O?O(d.el,T,D):D()}else T()},wt=(d,p)=>{let _;for(;d!==p;)_=h(d),o(d),d=_;o(p)},it=(d,p,_)=>{const{bum:E,scope:x,update:T,subTree:H,um:O}=d;E&&Hr(E),x.stop(),T&&(T.active=!1,Fe(H,d,p,_)),O&&Ue(O,p),Ue(()=>{d.isUnmounted=!0},p),p&&p.pendingBranch&&!p.isUnmounted&&d.asyncDep&&!d.asyncResolved&&d.suspenseId===p.pendingId&&(p.deps--,p.deps===0&&p.resolve())},$e=(d,p,_,E=!1,x=!1,T=0)=>{for(let H=T;Hd.shapeFlag&6?k(d.component.subTree):d.shapeFlag&128?d.suspense.next():h(d.anchor||d.el),B=(d,p,_)=>{d==null?p._vnode&&Fe(p._vnode,null,null,!0):w(p._vnode||null,d,p,null,null,null,_),ss(),dr(),p._vnode=d},N={p:w,um:Fe,m:Ve,r:Et,mt:I,mc:m,pc:se,pbc:M,n:k,o:e};let K,ce;return t&&([K,ce]=t(N)),{render:B,hydrate:K,createApp:$c(B,K)}}function Mt({effect:e,update:t},n){e.allowRecurse=t.allowRecurse=n}function ri(e,t,n=!1){const r=e.children,o=t.children;if(Q(r)&&Q(o))for(let s=0;s>1,e[n[i]]0&&(t[r]=n[s-1]),n[s]=r)}}for(s=n.length,l=n[s-1];s-- >0;)n[s]=l,l=t[l];return n}const Uc=e=>e.__isTeleport,we=Symbol.for("v-fgt"),un=Symbol.for("v-txt"),Ye=Symbol.for("v-cmt"),Tn=Symbol.for("v-stc"),Sn=[];let rt=null;function j(e=!1){Sn.push(rt=e?null:[])}function qc(){Sn.pop(),rt=Sn[Sn.length-1]||null}let Nn=1;function vs(e){Nn+=e}function oi(e){return e.dynamicChildren=Nn>0?rt||rn:null,qc(),Nn>0&&rt&&rt.push(e),e}function X(e,t,n,r,o,s){return oi(he(e,t,n,r,o,s,!0))}function Se(e,t,n,r,o){return oi(te(e,t,n,r,o,!0))}function _r(e){return e?e.__v_isVNode===!0:!1}function zt(e,t){return e.type===t.type&&e.key===t.key}const Ir="__vInternal",si=({key:e})=>e??null,ir=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?ge(e)||Ie(e)||oe(e)?{i:Me,r:e,k:t,f:!!n}:e:null);function he(e,t=null,n=null,r=0,o=null,s=e===we?0:1,l=!1,i=!1){const a={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&si(t),ref:t&&ir(t),scopeId:Bl,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetAnchor:null,staticCount:0,shapeFlag:s,patchFlag:r,dynamicProps:o,dynamicChildren:null,appContext:null,ctx:Me};return i?(Io(a,n),s&128&&e.normalize(a)):n&&(a.shapeFlag|=ge(n)?8:16),Nn>0&&!l&&rt&&(a.patchFlag>0||s&6)&&a.patchFlag!==32&&rt.push(a),a}const te=Wc;function Wc(e,t=null,n=null,r=0,o=null,s=!1){if((!e||e===Lc)&&(e=Ye),_r(e)){const i=$t(e,t,!0);return n&&Io(i,n),Nn>0&&!s&&rt&&(i.shapeFlag&6?rt[rt.indexOf(e)]=i:rt.push(i)),i.patchFlag|=-2,i}if(ru(e)&&(e=e.__vccOpts),t){t=Kc(t);let{class:i,style:a}=t;i&&!ge(i)&&(t.class=qe(i)),ke(a)&&(Ol(a)&&!Q(a)&&(a=Ae({},a)),t.style=jn(a))}const l=ge(e)?1:cc(e)?128:Uc(e)?64:ke(e)?4:oe(e)?2:0;return he(e,t,n,r,o,l,s,!0)}function Kc(e){return e?Ol(e)||Ir in e?Ae({},e):e:null}function $t(e,t,n=!1){const{props:r,ref:o,patchFlag:s,children:l}=e,i=t?co(r||{},t):r;return{__v_isVNode:!0,__v_skip:!0,type:e.type,props:i,key:i&&si(i),ref:t&&t.ref?n&&o?Q(o)?o.concat(ir(t)):[o,ir(t)]:ir(t):o,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:l,target:e.target,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==we?s===-1?16:s|16:s,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:e.transition,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&$t(e.ssContent),ssFallback:e.ssFallback&&$t(e.ssFallback),el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce}}function Nt(e=" ",t=0){return te(un,null,e,t)}function Gc(e,t){const n=te(Tn,null,e);return n.staticCount=t,n}function Ce(e="",t=!1){return t?(j(),Se(Ye,null,e)):te(Ye,null,e)}function tt(e){return e==null||typeof e=="boolean"?te(Ye):Q(e)?te(we,null,e.slice()):typeof e=="object"?Tt(e):te(un,null,String(e))}function Tt(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:$t(e)}function Io(e,t){let n=0;const{shapeFlag:r}=e;if(t==null)t=null;else if(Q(t))n=16;else if(typeof t=="object")if(r&65){const o=t.default;o&&(o._c&&(o._d=!1),Io(e,o()),o._c&&(o._d=!0));return}else{n=32;const o=t._;!o&&!(Ir in t)?t._ctx=Me:o===3&&Me&&(Me.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else oe(t)?(t={default:t,_ctx:Me},n=32):(t=String(t),r&64?(n=16,t=[Nt(t)]):n=8);e.children=t,e.shapeFlag|=n}function co(...e){const t={};for(let n=0;nOe||Me;let $o,Xt,_s="__VUE_INSTANCE_SETTERS__";(Xt=Zr()[_s])||(Xt=Zr()[_s]=[]),Xt.push(e=>Oe=e),$o=e=>{Xt.length>1?Xt.forEach(t=>t(e)):Xt[0](e)};const fn=e=>{$o(e),e.scope.on()},Kt=()=>{Oe&&Oe.scope.off(),$o(null)};function ii(e){return e.vnode.shapeFlag&4}let dn=!1;function Zc(e,t=!1){dn=t;const{props:n,children:r}=e.vnode,o=ii(e);Nc(e,n,o,t),Hc(e,r);const s=o?Xc(e,t):void 0;return dn=!1,s}function Xc(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=Rl(new Proxy(e.ctx,Tc));const{setup:r}=n;if(r){const o=e.setupContext=r.length>1?tu(e):null;fn(e),mn();const s=Ot(r,e,0,[e.props,o]);if(gn(),Kt(),ml(s)){if(s.then(Kt,Kt),t)return s.then(l=>{bs(e,l,t)}).catch(l=>{zn(l,e,0)});e.asyncDep=s}else bs(e,s,t)}else ai(e,t)}function bs(e,t,n){oe(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:ke(t)&&(e.setupState=Nl(t)),ai(e,n)}let ys;function ai(e,t,n){const r=e.type;if(!e.render){if(!t&&ys&&!r.render){const o=r.template||Oo(e).template;if(o){const{isCustomElement:s,compilerOptions:l}=e.appContext.config,{delimiters:i,compilerOptions:a}=r,c=Ae(Ae({isCustomElement:s,delimiters:i},l),a);r.render=ys(o,c)}}e.render=r.render||ot}fn(e),mn(),Sc(e),gn(),Kt()}function eu(e){return e.attrsProxy||(e.attrsProxy=new Proxy(e.attrs,{get(t,n){return We(e,"get","$attrs"),t[n]}}))}function tu(e){const t=n=>{e.exposed=n||{}};return{get attrs(){return eu(e)},slots:e.slots,emit:e.emit,expose:t}}function $r(e){if(e.exposed)return e.exposeProxy||(e.exposeProxy=new Proxy(Nl(Rl(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in Cn)return Cn[n](e)},has(t,n){return n in t||n in Cn}}))}function nu(e,t=!0){return oe(e)?e.displayName||e.name:e.name||t&&e.__name}function ru(e){return oe(e)&&"__vccOpts"in e}const F=(e,t)=>Za(e,t,dn);function ae(e,t,n){const r=arguments.length;return r===2?ke(t)&&!Q(t)?_r(t)?te(e,null,[t]):te(e,t):te(e,null,t):(r>3?n=Array.prototype.slice.call(arguments,2):r===3&&_r(n)&&(n=[n]),te(e,t,n))}const ou=Symbol.for("v-scx"),su=()=>Pe(ou),lu="3.3.4",iu="http://www.w3.org/2000/svg",Vt=typeof document<"u"?document:null,Es=Vt&&Vt.createElement("template"),au={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,r)=>{const o=t?Vt.createElementNS(iu,e):Vt.createElement(e,n?{is:n}:void 0);return e==="select"&&r&&r.multiple!=null&&o.setAttribute("multiple",r.multiple),o},createText:e=>Vt.createTextNode(e),createComment:e=>Vt.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Vt.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,r,o,s){const l=n?n.previousSibling:t.lastChild;if(o&&(o===s||o.nextSibling))for(;t.insertBefore(o.cloneNode(!0),n),!(o===s||!(o=o.nextSibling)););else{Es.innerHTML=r?`${e}`:e;const i=Es.content;if(r){const a=i.firstChild;for(;a.firstChild;)i.appendChild(a.firstChild);i.removeChild(a)}t.insertBefore(i,n)}return[l?l.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}};function cu(e,t,n){const r=e._vtc;r&&(t=(t?[t,...r]:[...r]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}function uu(e,t,n){const r=e.style,o=ge(n);if(n&&!o){if(t&&!ge(t))for(const s in t)n[s]==null&&uo(r,s,"");for(const s in n)uo(r,s,n[s])}else{const s=r.display;o?t!==n&&(r.cssText=n):t&&e.removeAttribute("style"),"_vod"in e&&(r.display=s)}}const ws=/\s*!important$/;function uo(e,t,n){if(Q(n))n.forEach(r=>uo(e,t,r));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const r=fu(e,t);ws.test(n)?e.setProperty(Yt(r),n.replace(ws,""),"important"):e[r]=n}}const ks=["Webkit","Moz","ms"],Vr={};function fu(e,t){const n=Vr[t];if(n)return n;let r=ft(t);if(r!=="filter"&&r in e)return Vr[t]=r;r=Lr(r);for(let o=0;oUr||(_u.then(()=>Ur=0),Ur=Date.now());function yu(e,t){const n=r=>{if(!r._vts)r._vts=Date.now();else if(r._vts<=n.attached)return;Xe(Eu(r,n.value),t,5,[r])};return n.value=e,n.attached=bu(),n}function Eu(e,t){if(Q(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(r=>o=>!o._stopped&&r&&r(o))}else return t}const Cs=/^on[a-z]/,wu=(e,t,n,r,o=!1,s,l,i,a)=>{t==="class"?cu(e,r,o):t==="style"?uu(e,n,r):Fn(t)?go(t)||gu(e,t,n,r,l):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):ku(e,t,r,o))?hu(e,t,r,s,l,i,a):(t==="true-value"?e._trueValue=r:t==="false-value"&&(e._falseValue=r),du(e,t,r,o))};function ku(e,t,n,r){return r?!!(t==="innerHTML"||t==="textContent"||t in e&&Cs.test(t)&&oe(n)):t==="spellcheck"||t==="draggable"||t==="translate"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA"||Cs.test(t)&&ge(n)?!1:t in e}const Lt="transition",bn="animation",qn=(e,{slots:t})=>ae(pc,xu(e),t);qn.displayName="Transition";const ci={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String};qn.props=Ae({},Ul,ci);const Ht=(e,t=[])=>{Q(e)?e.forEach(n=>n(...t)):e&&e(...t)},Ts=e=>e?Q(e)?e.some(t=>t.length>1):e.length>1:!1;function xu(e){const t={};for(const L in e)L in ci||(t[L]=e[L]);if(e.css===!1)return t;const{name:n="v",type:r,duration:o,enterFromClass:s=`${n}-enter-from`,enterActiveClass:l=`${n}-enter-active`,enterToClass:i=`${n}-enter-to`,appearFromClass:a=s,appearActiveClass:c=l,appearToClass:u=i,leaveFromClass:f=`${n}-leave-from`,leaveActiveClass:h=`${n}-leave-active`,leaveToClass:g=`${n}-leave-to`}=e,y=Lu(o),w=y&&y[0],C=y&&y[1],{onBeforeEnter:v,onEnter:b,onEnterCancelled:P,onLeave:S,onLeaveCancelled:U,onBeforeAppear:Z=v,onAppear:$=b,onAppearCancelled:m=P}=t,z=(L,R,I)=>{Ft(L,R?u:i),Ft(L,R?c:l),I&&I()},M=(L,R)=>{L._isLeaving=!1,Ft(L,f),Ft(L,g),Ft(L,h),R&&R()},G=L=>(R,I)=>{const le=L?$:b,V=()=>z(R,L,I);Ht(le,[R,V]),Ss(()=>{Ft(R,L?a:s),Ct(R,L?u:i),Ts(le)||Ps(R,r,w,V)})};return Ae(t,{onBeforeEnter(L){Ht(v,[L]),Ct(L,s),Ct(L,l)},onBeforeAppear(L){Ht(Z,[L]),Ct(L,a),Ct(L,c)},onEnter:G(!1),onAppear:G(!0),onLeave(L,R){L._isLeaving=!0;const I=()=>M(L,R);Ct(L,f),Su(),Ct(L,h),Ss(()=>{L._isLeaving&&(Ft(L,f),Ct(L,g),Ts(S)||Ps(L,r,C,I))}),Ht(S,[L,I])},onEnterCancelled(L){z(L,!1),Ht(P,[L])},onAppearCancelled(L){z(L,!0),Ht(m,[L])},onLeaveCancelled(L){M(L),Ht(U,[L])}})}function Lu(e){if(e==null)return null;if(ke(e))return[qr(e.enter),qr(e.leave)];{const t=qr(e);return[t,t]}}function qr(e){return la(e)}function Ct(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e._vtc||(e._vtc=new Set)).add(t)}function Ft(e,t){t.split(/\s+/).forEach(r=>r&&e.classList.remove(r));const{_vtc:n}=e;n&&(n.delete(t),n.size||(e._vtc=void 0))}function Ss(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let Cu=0;function Ps(e,t,n,r){const o=e._endId=++Cu,s=()=>{o===e._endId&&r()};if(n)return setTimeout(s,n);const{type:l,timeout:i,propCount:a}=Tu(e,t);if(!l)return r();const c=l+"end";let u=0;const f=()=>{e.removeEventListener(c,h),s()},h=g=>{g.target===e&&++u>=a&&f()};setTimeout(()=>{u(n[y]||"").split(", "),o=r(`${Lt}Delay`),s=r(`${Lt}Duration`),l=As(o,s),i=r(`${bn}Delay`),a=r(`${bn}Duration`),c=As(i,a);let u=null,f=0,h=0;t===Lt?l>0&&(u=Lt,f=l,h=s.length):t===bn?c>0&&(u=bn,f=c,h=a.length):(f=Math.max(l,c),u=f>0?l>c?Lt:bn:null,h=u?u===Lt?s.length:a.length:0);const g=u===Lt&&/\b(transform|all)(,|$)/.test(r(`${Lt}Property`).toString());return{type:u,timeout:f,propCount:h,hasTransform:g}}function As(e,t){for(;e.lengthOs(n)+Os(e[r])))}function Os(e){return Number(e.slice(0,-1).replace(",","."))*1e3}function Su(){return document.body.offsetHeight}const Pu={esc:"escape",space:" ",up:"arrow-up",left:"arrow-left",right:"arrow-right",down:"arrow-down",delete:"backspace"},Au=(e,t)=>n=>{if(!("key"in n))return;const r=Yt(n.key);if(t.some(o=>o===r||Pu[o]===r))return e(n)},br={beforeMount(e,{value:t},{transition:n}){e._vod=e.style.display==="none"?"":e.style.display,n&&t?n.beforeEnter(e):yn(e,t)},mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)},updated(e,{value:t,oldValue:n},{transition:r}){!t!=!n&&(r?t?(r.beforeEnter(e),yn(e,!0),r.enter(e)):r.leave(e,()=>{yn(e,!1)}):yn(e,t))},beforeUnmount(e,{value:t}){yn(e,t)}};function yn(e,t){e.style.display=t?e._vod:"none"}const Ou=Ae({patchProp:wu},au);let Wr,Rs=!1;function Ru(){return Wr=Rs?Wr:Bc(Ou),Rs=!0,Wr}const Iu=(...e)=>{const t=Ru().createApp(...e),{mount:n}=t;return t.mount=r=>{const o=$u(r);if(o)return n(o,!0,o instanceof SVGElement)},t};function $u(e){return ge(e)?document.querySelector(e):e}const Nu={"v-fe360bd6":()=>W(()=>import("./develop.html-117f0576.js"),[]).then(({data:e})=>e),"v-d08d435a":()=>W(()=>import("./introduce.html-93fae745.js"),[]).then(({data:e})=>e),"v-8daa1a0e":()=>W(()=>import("./index.html-29be9b02.js"),[]).then(({data:e})=>e),"v-53c50d87":()=>W(()=>import("./appium_lab.html-af1255b2.js"),[]).then(({data:e})=>e),"v-483ce5fe":()=>W(()=>import("./extensions.html-2fc302d1.js"),[]).then(({data:e})=>e),"v-299549e4":()=>W(()=>import("./page_object.html-cb039342.js"),[]).then(({data:e})=>e),"v-4370387b":()=>W(()=>import("./start.html-ea9f18a1.js"),[]).then(({data:e})=>e),"v-7cbb39b9":()=>W(()=>import("./api_case.html-ba462121.js"),[]).then(({data:e})=>e),"v-109501ec":()=>W(()=>import("./api_object.html-2747dbac.js"),[]).then(({data:e})=>e),"v-d24a86f0":()=>W(()=>import("./assert.html-657c6f15.js"),[]).then(({data:e})=>e),"v-5b62ef19":()=>W(()=>import("./more.html-cc1b2581.js"),[]).then(({data:e})=>e),"v-23f9483c":()=>W(()=>import("./start.html-b31eab27.js"),[]).then(({data:e})=>e),"v-488b8cec":()=>W(()=>import("./webscocket.html-50b19ef9.js"),[]).then(({data:e})=>e),"v-d8f79a72":()=>W(()=>import("./advanced.html-c039b7bf.js"),[]).then(({data:e})=>e),"v-8e520eda":()=>W(()=>import("./create_project.html-3536f4e7.js"),[]).then(({data:e})=>e),"v-78c619cc":()=>W(()=>import("./data_driver.html-7c198d0a.js"),[]).then(({data:e})=>e),"v-4e6f0425":()=>W(()=>import("./dependent_func.html-f699bc84.js"),[]).then(({data:e})=>e),"v-4e8563af":()=>W(()=>import("./installation.html-02312a74.js"),[]).then(({data:e})=>e),"v-0f898c79":()=>W(()=>import("./quick_start.html-87f1da62.js"),[]).then(({data:e})=>e),"v-6f32df80":()=>W(()=>import("./seldom_cli.html-fcf52dd5.js"),[]).then(({data:e})=>e),"v-3cdc5c3a":()=>W(()=>import("./platform.html-f7529303.js"),[]).then(({data:e})=>e),"v-c350a662":()=>W(()=>import("./db_operation.html-55b16cfc.js"),[]).then(({data:e})=>e),"v-1d715ea7":()=>W(()=>import("./test_library.html-f4b42a84.js"),[]).then(({data:e})=>e),"v-471218ee":()=>W(()=>import("./browser_driver.html-3b18ca47.js"),[]).then(({data:e})=>e),"v-198befa7":()=>W(()=>import("./chaining.html-c3eac4c8.js"),[]).then(({data:e})=>e),"v-1f2c830c":()=>W(()=>import("./other.html-5da407eb.js"),[]).then(({data:e})=>e),"v-0ccdb93b":()=>W(()=>import("./page_object.html-a01824ef.js"),[]).then(({data:e})=>e),"v-40ed12b6":()=>W(()=>import("./seldom_api.html-c0b2694d.js"),[]).then(({data:e})=>e),"v-129a7066":()=>W(()=>import("./CHANGES.html-65323456.js"),[]).then(({data:e})=>e),"v-3706649a":()=>W(()=>import("./404.html-60b35caa.js"),[]).then(({data:e})=>e)},Du=JSON.parse('{"base":"/","lang":"en-US","title":"seldom文档","description":"seldom 是基于unittest 的自动化测试框架。","head":[["link",{"rel":"icon","href":"/logo.jpeg"}]],"locales":{}}');var Mu=([e,t,n])=>e==="meta"&&t.name?`${e}.${t.name}`:["title","base"].includes(e)?e:e==="template"&&t.id?`${e}.${t.id}`:JSON.stringify([e,t,n]),Hu=e=>{const t=new Set,n=[];return e.forEach(r=>{const o=Mu(r);t.has(o)||(t.add(o),n.push(r))}),n},Wn=e=>/^(https?:)?\/\//.test(e),Fu=e=>/^mailto:/.test(e),ju=e=>/^tel:/.test(e),No=e=>Object.prototype.toString.call(e)==="[object Object]",ui=e=>e[e.length-1]==="/"?e.slice(0,-1):e,fi=e=>e[0]==="/"?e.slice(1):e,di=(e,t)=>{const n=Object.keys(e).sort((r,o)=>{const s=o.split("/").length-r.split("/").length;return s!==0?s:o.length-r.length});for(const r of n)if(t.startsWith(r))return r;return"/"};const hi={"v-fe360bd6":ve(()=>W(()=>import("./develop.html-45b4d86e.js"),[])),"v-d08d435a":ve(()=>W(()=>import("./introduce.html-a72107fb.js"),[])),"v-8daa1a0e":ve(()=>W(()=>import("./index.html-7f9a87de.js"),[])),"v-53c50d87":ve(()=>W(()=>import("./appium_lab.html-ab270943.js"),[])),"v-483ce5fe":ve(()=>W(()=>import("./extensions.html-58895b7c.js"),[])),"v-299549e4":ve(()=>W(()=>import("./page_object.html-c3c4d18b.js"),[])),"v-4370387b":ve(()=>W(()=>import("./start.html-a4c1b3f8.js"),[])),"v-7cbb39b9":ve(()=>W(()=>import("./api_case.html-1c2fbfd8.js"),[])),"v-109501ec":ve(()=>W(()=>import("./api_object.html-3082ec6d.js"),[])),"v-d24a86f0":ve(()=>W(()=>import("./assert.html-54753e7a.js"),[])),"v-5b62ef19":ve(()=>W(()=>import("./more.html-f6bf5e51.js"),[])),"v-23f9483c":ve(()=>W(()=>import("./start.html-dc3af13e.js"),[])),"v-488b8cec":ve(()=>W(()=>import("./webscocket.html-8dbaee79.js"),[])),"v-d8f79a72":ve(()=>W(()=>import("./advanced.html-c2d9b916.js"),[])),"v-8e520eda":ve(()=>W(()=>import("./create_project.html-385e2a55.js"),[])),"v-78c619cc":ve(()=>W(()=>import("./data_driver.html-b253800d.js"),[])),"v-4e6f0425":ve(()=>W(()=>import("./dependent_func.html-974e3a57.js"),[])),"v-4e8563af":ve(()=>W(()=>import("./installation.html-9ca7cf6d.js"),[])),"v-0f898c79":ve(()=>W(()=>import("./quick_start.html-c208f012.js"),[])),"v-6f32df80":ve(()=>W(()=>import("./seldom_cli.html-e3f8243b.js"),[])),"v-3cdc5c3a":ve(()=>W(()=>import("./platform.html-749be776.js"),[])),"v-c350a662":ve(()=>W(()=>import("./db_operation.html-e0a4c7c7.js"),[])),"v-1d715ea7":ve(()=>W(()=>import("./test_library.html-70c22988.js"),[])),"v-471218ee":ve(()=>W(()=>import("./browser_driver.html-299e0e9f.js"),[])),"v-198befa7":ve(()=>W(()=>import("./chaining.html-5cd990f6.js"),[])),"v-1f2c830c":ve(()=>W(()=>import("./other.html-8f9d5c4b.js"),[])),"v-0ccdb93b":ve(()=>W(()=>import("./page_object.html-e18b992a.js"),[])),"v-40ed12b6":ve(()=>W(()=>import("./seldom_api.html-a9e2c335.js"),[])),"v-129a7066":ve(()=>W(()=>import("./CHANGES.html-df6b9525.js"),[])),"v-3706649a":ve(()=>W(()=>import("./404.html-0e029f6a.js"),[]))};var Bu=Symbol(""),zu=me(Nu),pi=Bn({key:"",path:"",title:"",lang:"",frontmatter:{},headers:[]}),St=me(pi),Gt=()=>St,mi=Symbol(""),gt=()=>{const e=Pe(mi);if(!e)throw new Error("usePageFrontmatter() is called without provider.");return e},gi=Symbol(""),Vu=()=>{const e=Pe(gi);if(!e)throw new Error("usePageHead() is called without provider.");return e},Uu=Symbol(""),vi=Symbol(""),qu=()=>{const e=Pe(vi);if(!e)throw new Error("usePageLang() is called without provider.");return e},_i=Symbol(""),Wu=()=>{const e=Pe(_i);if(!e)throw new Error("usePageLayout() is called without provider.");return e},Do=Symbol(""),Kn=()=>{const e=Pe(Do);if(!e)throw new Error("useRouteLocale() is called without provider.");return e},nn=me(Du),bi=()=>nn,yi=Symbol(""),Mo=()=>{const e=Pe(yi);if(!e)throw new Error("useSiteLocaleData() is called without provider.");return e},Ku=Symbol(""),Gu="Layout",Yu="NotFound",ht=vn({resolveLayouts:e=>e.reduce((t,n)=>({...t,...n.layouts}),{}),resolvePageData:async e=>{const t=zu.value[e];return await(t==null?void 0:t())??pi},resolvePageFrontmatter:e=>e.frontmatter,resolvePageHead:(e,t,n)=>{const r=ge(t.description)?t.description:n.description,o=[...Q(t.head)?t.head:[],...n.head,["title",{},e],["meta",{name:"description",content:r}]];return Hu(o)},resolvePageHeadTitle:(e,t)=>[e.title,t.title].filter(n=>!!n).join(" | "),resolvePageLang:e=>e.lang||"en",resolvePageLayout:(e,t)=>{let n;if(e.path){const r=e.frontmatter.layout;ge(r)?n=r:n=Gu}else n=Yu;return t[n]},resolveRouteLocale:(e,t)=>di(e,t),resolveSiteLocaleData:(e,t)=>({...e,...e.locales[t]})}),Ho=ue({name:"ClientOnly",setup(e,t){const n=me(!1);return Je(()=>{n.value=!0}),()=>{var r,o;return n.value?(o=(r=t.slots).default)==null?void 0:o.call(r):null}}}),Ju=ue({name:"Content",props:{pageKey:{type:String,required:!1,default:""}},setup(e){const t=Gt(),n=F(()=>hi[e.pageKey||t.value.key]);return()=>n.value?ae(n.value):ae("div","404 Not Found")}}),Dt=(e={})=>e,Fo=e=>Wn(e)?e:`/${fi(e)}`;function Ei(e,t,n){var r,o,s;t===void 0&&(t=50),n===void 0&&(n={});var l=(r=n.isImmediate)!=null&&r,i=(o=n.callback)!=null&&o,a=n.maxWait,c=Date.now(),u=[];function f(){if(a!==void 0){var g=Date.now()-c;if(g+t>=a)return a-g}return t}var h=function(){var g=[].slice.call(arguments),y=this;return new Promise(function(w,C){var v=l&&s===void 0;if(s!==void 0&&clearTimeout(s),s=setTimeout(function(){if(s=void 0,c=Date.now(),!l){var P=e.apply(y,g);i&&i(P),u.forEach(function(S){return(0,S.resolve)(P)}),u=[]}},f()),v){var b=e.apply(y,g);return i&&i(b),w(b)}u.push({resolve:w,reject:C})})};return h.cancel=function(g){s!==void 0&&clearTimeout(s),u.forEach(function(y){return(0,y.reject)(g)}),u=[]},h}/*! +const Qi="modulepreload",Zi=function(e){return"/"+e},Jo={},W=function(t,n,r){if(!n||n.length===0)return t();const o=document.getElementsByTagName("link");return Promise.all(n.map(s=>{if(s=Zi(s),s in Jo)return;Jo[s]=!0;const l=s.endsWith(".css"),i=l?'[rel="stylesheet"]':"";if(!!r)for(let u=o.length-1;u>=0;u--){const f=o[u];if(f.href===s&&(!l||f.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${s}"]${i}`))return;const c=document.createElement("link");if(c.rel=l?"stylesheet":Qi,l||(c.as="script",c.crossOrigin=""),c.href=s,document.head.appendChild(c),l)return new Promise((u,f)=>{c.addEventListener("load",u),c.addEventListener("error",()=>f(new Error(`Unable to preload CSS for ${s}`)))})})).then(()=>t())};function mo(e,t){const n=Object.create(null),r=e.split(",");for(let o=0;o!!n[o.toLowerCase()]:o=>!!n[o]}const xe={},rn=[],ot=()=>{},Xi=()=>!1,ea=/^on[^a-z]/,Fn=e=>ea.test(e),go=e=>e.startsWith("onUpdate:"),Ae=Object.assign,vo=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},ta=Object.prototype.hasOwnProperty,de=(e,t)=>ta.call(e,t),Q=Array.isArray,on=e=>kr(e)==="[object Map]",pl=e=>kr(e)==="[object Set]",oe=e=>typeof e=="function",ge=e=>typeof e=="string",_o=e=>typeof e=="symbol",ke=e=>e!==null&&typeof e=="object",ml=e=>ke(e)&&oe(e.then)&&oe(e.catch),gl=Object.prototype.toString,kr=e=>gl.call(e),na=e=>kr(e).slice(8,-1),vl=e=>kr(e)==="[object Object]",bo=e=>ge(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,Ln=mo(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),xr=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},ra=/-(\w)/g,ft=xr(e=>e.replace(ra,(t,n)=>n?n.toUpperCase():"")),oa=/\B([A-Z])/g,Yt=xr(e=>e.replace(oa,"-$1").toLowerCase()),Lr=xr(e=>e.charAt(0).toUpperCase()+e.slice(1)),Mr=xr(e=>e?`on${Lr(e)}`:""),On=(e,t)=>!Object.is(e,t),Hr=(e,t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,value:n})},sa=e=>{const t=parseFloat(e);return isNaN(t)?e:t},la=e=>{const t=ge(e)?Number(e):NaN;return isNaN(t)?e:t};let Qo;const Zr=()=>Qo||(Qo=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function jn(e){if(Q(e)){const t={};for(let n=0;n{if(n){const r=n.split(aa);r.length>1&&(t[r[0].trim()]=r[1].trim())}}),t}function qe(e){let t="";if(ge(e))t=e;else if(Q(e))for(let n=0;nge(e)?e:e==null?"":Q(e)||ke(e)&&(e.toString===gl||!oe(e.toString))?JSON.stringify(e,bl,2):String(e),bl=(e,t)=>t&&t.__v_isRef?bl(e,t.value):on(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[r,o])=>(n[`${r} =>`]=o,n),{})}:pl(t)?{[`Set(${t.size})`]:[...t.values()]}:ke(t)&&!Q(t)&&!vl(t)?String(t):t;let Ge;class ha{constructor(t=!1){this.detached=t,this._active=!0,this.effects=[],this.cleanups=[],this.parent=Ge,!t&&Ge&&(this.index=(Ge.scopes||(Ge.scopes=[])).push(this)-1)}get active(){return this._active}run(t){if(this._active){const n=Ge;try{return Ge=this,t()}finally{Ge=n}}}on(){Ge=this}off(){Ge=this.parent}stop(t){if(this._active){let n,r;for(n=0,r=this.effects.length;n{const t=new Set(e);return t.w=0,t.n=0,t},El=e=>(e.w&Rt)>0,wl=e=>(e.n&Rt)>0,ga=({deps:e})=>{if(e.length)for(let t=0;t{const{deps:t}=e;if(t.length){let n=0;for(let r=0;r{(u==="length"||u>=a)&&i.push(c)})}else switch(n!==void 0&&i.push(l.get(n)),t){case"add":Q(e)?bo(n)&&i.push(l.get("length")):(i.push(l.get(qt)),on(e)&&i.push(l.get(eo)));break;case"delete":Q(e)||(i.push(l.get(qt)),on(e)&&i.push(l.get(eo)));break;case"set":on(e)&&i.push(l.get(qt));break}if(i.length===1)i[0]&&to(i[0]);else{const a=[];for(const c of i)c&&a.push(...c);to(yo(a))}}function to(e,t){const n=Q(e)?e:[...e];for(const r of n)r.computed&&Xo(r);for(const r of n)r.computed||Xo(r)}function Xo(e,t){(e!==nt||e.allowRecurse)&&(e.scheduler?e.scheduler():e.run())}function _a(e,t){var n;return(n=ur.get(e))==null?void 0:n.get(t)}const ba=mo("__proto__,__v_isRef,__isVue"),Ll=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(_o)),ya=wo(),Ea=wo(!1,!0),wa=wo(!0),es=ka();function ka(){const e={};return["includes","indexOf","lastIndexOf"].forEach(t=>{e[t]=function(...n){const r=pe(this);for(let s=0,l=this.length;s{e[t]=function(...n){mn();const r=pe(this)[t].apply(this,n);return gn(),r}}),e}function xa(e){const t=pe(this);return We(t,"has",e),t.hasOwnProperty(e)}function wo(e=!1,t=!1){return function(r,o,s){if(o==="__v_isReactive")return!e;if(o==="__v_isReadonly")return e;if(o==="__v_isShallow")return t;if(o==="__v_raw"&&s===(e?t?ja:Al:t?Pl:Sl).get(r))return r;const l=Q(r);if(!e){if(l&&de(es,o))return Reflect.get(es,o,s);if(o==="hasOwnProperty")return xa}const i=Reflect.get(r,o,s);return(_o(o)?Ll.has(o):ba(o))||(e||We(r,"get",o),t)?i:Ie(i)?l&&bo(o)?i:i.value:ke(i)?e?Bn(i):vn(i):i}}const La=Cl(),Ca=Cl(!0);function Cl(e=!1){return function(n,r,o,s){let l=n[r];if(cn(l)&&Ie(l)&&!Ie(o))return!1;if(!e&&(!fr(o)&&!cn(o)&&(l=pe(l),o=pe(o)),!Q(n)&&Ie(l)&&!Ie(o)))return l.value=o,!0;const i=Q(n)&&bo(r)?Number(r)e,Cr=e=>Reflect.getPrototypeOf(e);function Gn(e,t,n=!1,r=!1){e=e.__v_raw;const o=pe(e),s=pe(t);n||(t!==s&&We(o,"get",t),We(o,"get",s));const{has:l}=Cr(o),i=r?ko:n?Co:Rn;if(l.call(o,t))return i(e.get(t));if(l.call(o,s))return i(e.get(s));e!==o&&e.get(t)}function Yn(e,t=!1){const n=this.__v_raw,r=pe(n),o=pe(e);return t||(e!==o&&We(r,"has",e),We(r,"has",o)),e===o?n.has(e):n.has(e)||n.has(o)}function Jn(e,t=!1){return e=e.__v_raw,!t&&We(pe(e),"iterate",qt),Reflect.get(e,"size",e)}function ts(e){e=pe(e);const t=pe(this);return Cr(t).has.call(t,e)||(t.add(e),_t(t,"add",e,e)),this}function ns(e,t){t=pe(t);const n=pe(this),{has:r,get:o}=Cr(n);let s=r.call(n,e);s||(e=pe(e),s=r.call(n,e));const l=o.call(n,e);return n.set(e,t),s?On(t,l)&&_t(n,"set",e,t):_t(n,"add",e,t),this}function rs(e){const t=pe(this),{has:n,get:r}=Cr(t);let o=n.call(t,e);o||(e=pe(e),o=n.call(t,e)),r&&r.call(t,e);const s=t.delete(e);return o&&_t(t,"delete",e,void 0),s}function os(){const e=pe(this),t=e.size!==0,n=e.clear();return t&&_t(e,"clear",void 0,void 0),n}function Qn(e,t){return function(r,o){const s=this,l=s.__v_raw,i=pe(l),a=t?ko:e?Co:Rn;return!e&&We(i,"iterate",qt),l.forEach((c,u)=>r.call(o,a(c),a(u),s))}}function Zn(e,t,n){return function(...r){const o=this.__v_raw,s=pe(o),l=on(s),i=e==="entries"||e===Symbol.iterator&&l,a=e==="keys"&&l,c=o[e](...r),u=n?ko:t?Co:Rn;return!t&&We(s,"iterate",a?eo:qt),{next(){const{value:f,done:h}=c.next();return h?{value:f,done:h}:{value:i?[u(f[0]),u(f[1])]:u(f),done:h}},[Symbol.iterator](){return this}}}}function kt(e){return function(...t){return e==="delete"?!1:this}}function Ra(){const e={get(s){return Gn(this,s)},get size(){return Jn(this)},has:Yn,add:ts,set:ns,delete:rs,clear:os,forEach:Qn(!1,!1)},t={get(s){return Gn(this,s,!1,!0)},get size(){return Jn(this)},has:Yn,add:ts,set:ns,delete:rs,clear:os,forEach:Qn(!1,!0)},n={get(s){return Gn(this,s,!0)},get size(){return Jn(this,!0)},has(s){return Yn.call(this,s,!0)},add:kt("add"),set:kt("set"),delete:kt("delete"),clear:kt("clear"),forEach:Qn(!0,!1)},r={get(s){return Gn(this,s,!0,!0)},get size(){return Jn(this,!0)},has(s){return Yn.call(this,s,!0)},add:kt("add"),set:kt("set"),delete:kt("delete"),clear:kt("clear"),forEach:Qn(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(s=>{e[s]=Zn(s,!1,!1),n[s]=Zn(s,!0,!1),t[s]=Zn(s,!1,!0),r[s]=Zn(s,!0,!0)}),[e,n,t,r]}const[Ia,$a,Na,Da]=Ra();function xo(e,t){const n=t?e?Da:Na:e?$a:Ia;return(r,o,s)=>o==="__v_isReactive"?!e:o==="__v_isReadonly"?e:o==="__v_raw"?r:Reflect.get(de(n,o)&&o in r?n:r,o,s)}const Ma={get:xo(!1,!1)},Ha={get:xo(!1,!0)},Fa={get:xo(!0,!1)},Sl=new WeakMap,Pl=new WeakMap,Al=new WeakMap,ja=new WeakMap;function Ba(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function za(e){return e.__v_skip||!Object.isExtensible(e)?0:Ba(na(e))}function vn(e){return cn(e)?e:Lo(e,!1,Tl,Ma,Sl)}function Va(e){return Lo(e,!1,Oa,Ha,Pl)}function Bn(e){return Lo(e,!0,Aa,Fa,Al)}function Lo(e,t,n,r,o){if(!ke(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const s=o.get(e);if(s)return s;const l=za(e);if(l===0)return e;const i=new Proxy(e,l===2?r:n);return o.set(e,i),i}function sn(e){return cn(e)?sn(e.__v_raw):!!(e&&e.__v_isReactive)}function cn(e){return!!(e&&e.__v_isReadonly)}function fr(e){return!!(e&&e.__v_isShallow)}function Ol(e){return sn(e)||cn(e)}function pe(e){const t=e&&e.__v_raw;return t?pe(t):e}function Rl(e){return cr(e,"__v_skip",!0),e}const Rn=e=>ke(e)?vn(e):e,Co=e=>ke(e)?Bn(e):e;function To(e){At&&nt&&(e=pe(e),xl(e.dep||(e.dep=yo())))}function So(e,t){e=pe(e);const n=e.dep;n&&to(n)}function Ie(e){return!!(e&&e.__v_isRef===!0)}function me(e){return $l(e,!1)}function Il(e){return $l(e,!0)}function $l(e,t){return Ie(e)?e:new Ua(e,t)}class Ua{constructor(t,n){this.__v_isShallow=n,this.dep=void 0,this.__v_isRef=!0,this._rawValue=n?t:pe(t),this._value=n?t:Rn(t)}get value(){return To(this),this._value}set value(t){const n=this.__v_isShallow||fr(t)||cn(t);t=n?t:pe(t),On(t,this._rawValue)&&(this._rawValue=t,this._value=n?t:Rn(t),So(this))}}function ee(e){return Ie(e)?e.value:e}const qa={get:(e,t,n)=>ee(Reflect.get(e,t,n)),set:(e,t,n,r)=>{const o=e[t];return Ie(o)&&!Ie(n)?(o.value=n,!0):Reflect.set(e,t,n,r)}};function Nl(e){return sn(e)?e:new Proxy(e,qa)}class Wa{constructor(t){this.dep=void 0,this.__v_isRef=!0;const{get:n,set:r}=t(()=>To(this),()=>So(this));this._get=n,this._set=r}get value(){return this._get()}set value(t){this._set(t)}}function Ka(e){return new Wa(e)}function Tr(e){const t=Q(e)?new Array(e.length):{};for(const n in e)t[n]=Dl(e,n);return t}class Ga{constructor(t,n,r){this._object=t,this._key=n,this._defaultValue=r,this.__v_isRef=!0}get value(){const t=this._object[this._key];return t===void 0?this._defaultValue:t}set value(t){this._object[this._key]=t}get dep(){return _a(pe(this._object),this._key)}}class Ya{constructor(t){this._getter=t,this.__v_isRef=!0,this.__v_isReadonly=!0}get value(){return this._getter()}}function Ja(e,t,n){return Ie(e)?e:oe(e)?new Ya(e):ke(e)&&arguments.length>1?Dl(e,t,n):me(e)}function Dl(e,t,n){const r=e[t];return Ie(r)?r:new Ga(e,t,n)}class Qa{constructor(t,n,r,o){this._setter=n,this.dep=void 0,this.__v_isRef=!0,this.__v_isReadonly=!1,this._dirty=!0,this.effect=new Eo(t,()=>{this._dirty||(this._dirty=!0,So(this))}),this.effect.computed=this,this.effect.active=this._cacheable=!o,this.__v_isReadonly=r}get value(){const t=pe(this);return To(t),(t._dirty||!t._cacheable)&&(t._dirty=!1,t._value=t.effect.run()),t._value}set value(t){this._setter(t)}}function Za(e,t,n=!1){let r,o;const s=oe(e);return s?(r=e,o=ot):(r=e.get,o=e.set),new Qa(r,o,s||!o,n)}function Ot(e,t,n,r){let o;try{o=r?e(...r):e()}catch(s){zn(s,t,n)}return o}function Xe(e,t,n,r){if(oe(e)){const s=Ot(e,t,n,r);return s&&ml(s)&&s.catch(l=>{zn(l,t,n)}),s}const o=[];for(let s=0;s>>1;$n(je[r])ut&&je.splice(t,1)}function nc(e){Q(e)?ln.push(...e):(!mt||!mt.includes(e,e.allowRecurse?Bt+1:Bt))&&ln.push(e),Hl()}function ss(e,t=In?ut+1:0){for(;t$n(n)-$n(r)),Bt=0;Bte.id==null?1/0:e.id,rc=(e,t)=>{const n=$n(e)-$n(t);if(n===0){if(e.pre&&!t.pre)return-1;if(t.pre&&!e.pre)return 1}return n};function Fl(e){no=!1,In=!0,je.sort(rc);const t=ot;try{for(ut=0;utge(g)?g.trim():g)),f&&(o=n.map(sa))}let i,a=r[i=Mr(t)]||r[i=Mr(ft(t))];!a&&s&&(a=r[i=Mr(Yt(t))]),a&&Xe(a,e,6,o);const c=r[i+"Once"];if(c){if(!e.emitted)e.emitted={};else if(e.emitted[i])return;e.emitted[i]=!0,Xe(c,e,6,o)}}function jl(e,t,n=!1){const r=t.emitsCache,o=r.get(e);if(o!==void 0)return o;const s=e.emits;let l={},i=!1;if(!oe(e)){const a=c=>{const u=jl(c,t,!0);u&&(i=!0,Ae(l,u))};!n&&t.mixins.length&&t.mixins.forEach(a),e.extends&&a(e.extends),e.mixins&&e.mixins.forEach(a)}return!s&&!i?(ke(e)&&r.set(e,null),null):(Q(s)?s.forEach(a=>l[a]=null):Ae(l,s),ke(e)&&r.set(e,l),l)}function Ar(e,t){return!e||!Fn(t)?!1:(t=t.slice(2).replace(/Once$/,""),de(e,t[0].toLowerCase()+t.slice(1))||de(e,Yt(t))||de(e,t))}let Me=null,Bl=null;function hr(e){const t=Me;return Me=e,Bl=e&&e.type.__scopeId||null,t}function De(e,t=Me,n){if(!t||e._n)return e;const r=(...o)=>{r._d&&vs(-1);const s=hr(t);let l;try{l=e(...o)}finally{hr(s),r._d&&vs(1)}return l};return r._n=!0,r._c=!0,r._d=!0,r}function Fr(e){const{type:t,vnode:n,proxy:r,withProxy:o,props:s,propsOptions:[l],slots:i,attrs:a,emit:c,render:u,renderCache:f,data:h,setupState:g,ctx:y,inheritAttrs:w}=e;let C,v;const b=hr(e);try{if(n.shapeFlag&4){const S=o||r;C=tt(u.call(S,S,f,s,g,h,y)),v=a}else{const S=t;C=tt(S.length>1?S(s,{attrs:a,slots:i,emit:c}):S(s,null)),v=t.props?a:sc(a)}}catch(S){Sn.length=0,zn(S,e,1),C=te(Ye)}let P=C;if(v&&w!==!1){const S=Object.keys(v),{shapeFlag:U}=P;S.length&&U&7&&(l&&S.some(go)&&(v=lc(v,l)),P=$t(P,v))}return n.dirs&&(P=$t(P),P.dirs=P.dirs?P.dirs.concat(n.dirs):n.dirs),n.transition&&(P.transition=n.transition),C=P,hr(b),C}const sc=e=>{let t;for(const n in e)(n==="class"||n==="style"||Fn(n))&&((t||(t={}))[n]=e[n]);return t},lc=(e,t)=>{const n={};for(const r in e)(!go(r)||!(r.slice(9)in t))&&(n[r]=e[r]);return n};function ic(e,t,n){const{props:r,children:o,component:s}=e,{props:l,children:i,patchFlag:a}=t,c=s.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&a>=0){if(a&1024)return!0;if(a&16)return r?ls(r,l,c):!!l;if(a&8){const u=t.dynamicProps;for(let f=0;fe.__isSuspense;function zl(e,t){t&&t.pendingBranch?Q(e)?t.effects.push(...e):t.effects.push(e):nc(e)}function uc(e,t){return Ao(e,null,t)}const Xn={};function st(e,t,n){return Ao(e,t,n)}function Ao(e,t,{immediate:n,deep:r,flush:o,onTrack:s,onTrigger:l}=xe){var i;const a=yl()===((i=Oe)==null?void 0:i.scope)?Oe:null;let c,u=!1,f=!1;if(Ie(e)?(c=()=>e.value,u=fr(e)):sn(e)?(c=()=>e,r=!0):Q(e)?(f=!0,u=e.some(S=>sn(S)||fr(S)),c=()=>e.map(S=>{if(Ie(S))return S.value;if(sn(S))return Ut(S);if(oe(S))return Ot(S,a,2)})):oe(e)?t?c=()=>Ot(e,a,2):c=()=>{if(!(a&&a.isUnmounted))return h&&h(),Xe(e,a,3,[g])}:c=ot,t&&r){const S=c;c=()=>Ut(S())}let h,g=S=>{h=b.onStop=()=>{Ot(S,a,4)}},y;if(dn)if(g=ot,t?n&&Xe(t,a,3,[c(),f?[]:void 0,g]):c(),o==="sync"){const S=su();y=S.__watcherHandles||(S.__watcherHandles=[])}else return ot;let w=f?new Array(e.length).fill(Xn):Xn;const C=()=>{if(b.active)if(t){const S=b.run();(r||u||(f?S.some((U,Z)=>On(U,w[Z])):On(S,w)))&&(h&&h(),Xe(t,a,3,[S,w===Xn?void 0:f&&w[0]===Xn?[]:w,g]),w=S)}else b.run()};C.allowRecurse=!!t;let v;o==="sync"?v=C:o==="post"?v=()=>Ue(C,a&&a.suspense):(C.pre=!0,a&&(C.id=a.uid),v=()=>Pr(C));const b=new Eo(c,v);t?n?C():w=b.run():o==="post"?Ue(b.run.bind(b),a&&a.suspense):b.run();const P=()=>{b.stop(),a&&a.scope&&vo(a.scope.effects,b)};return y&&y.push(P),P}function fc(e,t,n){const r=this.proxy,o=ge(e)?e.includes(".")?Vl(r,e):()=>r[e]:e.bind(r,r);let s;oe(t)?s=t:(s=t.handler,n=t);const l=Oe;fn(this);const i=Ao(o,s.bind(r),n);return l?fn(l):Kt(),i}function Vl(e,t){const n=t.split(".");return()=>{let r=e;for(let o=0;o{Ut(n,t)});else if(vl(e))for(const n in e)Ut(e[n],t);return e}function pr(e,t){const n=Me;if(n===null)return e;const r=$r(n)||n.proxy,o=e.dirs||(e.dirs=[]);for(let s=0;s{e.isMounted=!0}),Un(()=>{e.isUnmounting=!0}),e}const Qe=[Function,Array],Ul={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:Qe,onEnter:Qe,onAfterEnter:Qe,onEnterCancelled:Qe,onBeforeLeave:Qe,onLeave:Qe,onAfterLeave:Qe,onLeaveCancelled:Qe,onBeforeAppear:Qe,onAppear:Qe,onAfterAppear:Qe,onAppearCancelled:Qe},hc={name:"BaseTransition",props:Ul,setup(e,{slots:t}){const n=li(),r=dc();let o;return()=>{const s=t.default&&Wl(t.default(),!0);if(!s||!s.length)return;let l=s[0];if(s.length>1){for(const w of s)if(w.type!==Ye){l=w;break}}const i=pe(e),{mode:a}=i;if(r.isLeaving)return jr(l);const c=is(l);if(!c)return jr(l);const u=ro(c,i,r,n);oo(c,u);const f=n.subTree,h=f&&is(f);let g=!1;const{getTransitionKey:y}=c.type;if(y){const w=y();o===void 0?o=w:w!==o&&(o=w,g=!0)}if(h&&h.type!==Ye&&(!zt(c,h)||g)){const w=ro(h,i,r,n);if(oo(h,w),a==="out-in")return r.isLeaving=!0,w.afterLeave=()=>{r.isLeaving=!1,n.update.active!==!1&&n.update()},jr(l);a==="in-out"&&c.type!==Ye&&(w.delayLeave=(C,v,b)=>{const P=ql(r,h);P[String(h.key)]=h,C._leaveCb=()=>{v(),C._leaveCb=void 0,delete u.delayedLeave},u.delayedLeave=b})}return l}}},pc=hc;function ql(e,t){const{leavingVNodes:n}=e;let r=n.get(t.type);return r||(r=Object.create(null),n.set(t.type,r)),r}function ro(e,t,n,r){const{appear:o,mode:s,persisted:l=!1,onBeforeEnter:i,onEnter:a,onAfterEnter:c,onEnterCancelled:u,onBeforeLeave:f,onLeave:h,onAfterLeave:g,onLeaveCancelled:y,onBeforeAppear:w,onAppear:C,onAfterAppear:v,onAppearCancelled:b}=t,P=String(e.key),S=ql(n,e),U=(m,z)=>{m&&Xe(m,r,9,z)},Z=(m,z)=>{const M=z[1];U(m,z),Q(m)?m.every(G=>G.length<=1)&&M():m.length<=1&&M()},$={mode:s,persisted:l,beforeEnter(m){let z=i;if(!n.isMounted)if(o)z=w||i;else return;m._leaveCb&&m._leaveCb(!0);const M=S[P];M&&zt(e,M)&&M.el._leaveCb&&M.el._leaveCb(),U(z,[m])},enter(m){let z=a,M=c,G=u;if(!n.isMounted)if(o)z=C||a,M=v||c,G=b||u;else return;let L=!1;const R=m._enterCb=I=>{L||(L=!0,I?U(G,[m]):U(M,[m]),$.delayedLeave&&$.delayedLeave(),m._enterCb=void 0)};z?Z(z,[m,R]):R()},leave(m,z){const M=String(e.key);if(m._enterCb&&m._enterCb(!0),n.isUnmounting)return z();U(f,[m]);let G=!1;const L=m._leaveCb=R=>{G||(G=!0,z(),R?U(y,[m]):U(g,[m]),m._leaveCb=void 0,S[M]===e&&delete S[M])};S[M]=e,h?Z(h,[m,L]):L()},clone(m){return ro(m,t,n,r)}};return $}function jr(e){if(Vn(e))return e=$t(e),e.children=null,e}function is(e){return Vn(e)?e.children?e.children[0]:void 0:e}function oo(e,t){e.shapeFlag&6&&e.component?oo(e.component.subTree,t):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function Wl(e,t=!1,n){let r=[],o=0;for(let s=0;s1)for(let s=0;sAe({name:e.name},t,{setup:e}))():e}const an=e=>!!e.type.__asyncLoader;function ve(e){oe(e)&&(e={loader:e});const{loader:t,loadingComponent:n,errorComponent:r,delay:o=200,timeout:s,suspensible:l=!0,onError:i}=e;let a=null,c,u=0;const f=()=>(u++,a=null,h()),h=()=>{let g;return a||(g=a=t().catch(y=>{if(y=y instanceof Error?y:new Error(String(y)),i)return new Promise((w,C)=>{i(y,()=>w(f()),()=>C(y),u+1)});throw y}).then(y=>g!==a&&a?a:(y&&(y.__esModule||y[Symbol.toStringTag]==="Module")&&(y=y.default),c=y,y)))};return ue({name:"AsyncComponentWrapper",__asyncLoader:h,get __asyncResolved(){return c},setup(){const g=Oe;if(c)return()=>Br(c,g);const y=b=>{a=null,zn(b,g,13,!r)};if(l&&g.suspense||dn)return h().then(b=>()=>Br(b,g)).catch(b=>(y(b),()=>r?te(r,{error:b}):null));const w=me(!1),C=me(),v=me(!!o);return o&&setTimeout(()=>{v.value=!1},o),s!=null&&setTimeout(()=>{if(!w.value&&!C.value){const b=new Error(`Async component timed out after ${s}ms.`);y(b),C.value=b}},s),h().then(()=>{w.value=!0,g.parent&&Vn(g.parent.vnode)&&Pr(g.parent.update)}).catch(b=>{y(b),C.value=b}),()=>{if(w.value&&c)return Br(c,g);if(C.value&&r)return te(r,{error:C.value});if(n&&!v.value)return te(n)}}})}function Br(e,t){const{ref:n,props:r,children:o,ce:s}=t.vnode,l=te(e,r,o);return l.ref=n,l.ce=s,delete t.vnode.ce,l}const Vn=e=>e.type.__isKeepAlive;function mc(e,t){Kl(e,"a",t)}function gc(e,t){Kl(e,"da",t)}function Kl(e,t,n=Oe){const r=e.__wdc||(e.__wdc=()=>{let o=n;for(;o;){if(o.isDeactivated)return;o=o.parent}return e()});if(Or(t,r,n),n){let o=n.parent;for(;o&&o.parent;)Vn(o.parent.vnode)&&vc(r,t,n,o),o=o.parent}}function vc(e,t,n,r){const o=Or(t,e,r,!0);Rr(()=>{vo(r[t],o)},n)}function Or(e,t,n=Oe,r=!1){if(n){const o=n[e]||(n[e]=[]),s=t.__weh||(t.__weh=(...l)=>{if(n.isUnmounted)return;mn(),fn(n);const i=Xe(t,n,e,l);return Kt(),gn(),i});return r?o.unshift(s):o.push(s),s}}const yt=e=>(t,n=Oe)=>(!dn||e==="sp")&&Or(e,(...r)=>t(...r),n),_c=yt("bm"),Je=yt("m"),bc=yt("bu"),yc=yt("u"),Un=yt("bum"),Rr=yt("um"),Ec=yt("sp"),wc=yt("rtg"),kc=yt("rtc");function xc(e,t=Oe){Or("ec",e,t)}const Gl="components";function bt(e,t){return Cc(Gl,e,!0,t)||e}const Lc=Symbol.for("v-ndc");function Cc(e,t,n=!0,r=!1){const o=Me||Oe;if(o){const s=o.type;if(e===Gl){const i=nu(s,!1);if(i&&(i===t||i===ft(t)||i===Lr(ft(t))))return s}const l=as(o[e]||s[e],t)||as(o.appContext[e],t);return!l&&r?s:l}}function as(e,t){return e&&(e[t]||e[ft(t)]||e[Lr(ft(t))])}function It(e,t,n,r){let o;const s=n&&n[r];if(Q(e)||ge(e)){o=new Array(e.length);for(let l=0,i=e.length;lt(l,i,void 0,s&&s[i]));else{const l=Object.keys(e);o=new Array(l.length);for(let i=0,a=l.length;i_r(t)?!(t.type===Ye||t.type===we&&!Yl(t.children)):!0)?e:null}const so=e=>e?ii(e)?$r(e)||e.proxy:so(e.parent):null,Cn=Ae(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>so(e.parent),$root:e=>so(e.root),$emit:e=>e.emit,$options:e=>Oo(e),$forceUpdate:e=>e.f||(e.f=()=>Pr(e.update)),$nextTick:e=>e.n||(e.n=Sr.bind(e.proxy)),$watch:e=>fc.bind(e)}),zr=(e,t)=>e!==xe&&!e.__isScriptSetup&&de(e,t),Tc={get({_:e},t){const{ctx:n,setupState:r,data:o,props:s,accessCache:l,type:i,appContext:a}=e;let c;if(t[0]!=="$"){const g=l[t];if(g!==void 0)switch(g){case 1:return r[t];case 2:return o[t];case 4:return n[t];case 3:return s[t]}else{if(zr(r,t))return l[t]=1,r[t];if(o!==xe&&de(o,t))return l[t]=2,o[t];if((c=e.propsOptions[0])&&de(c,t))return l[t]=3,s[t];if(n!==xe&&de(n,t))return l[t]=4,n[t];lo&&(l[t]=0)}}const u=Cn[t];let f,h;if(u)return t==="$attrs"&&We(e,"get",t),u(e);if((f=i.__cssModules)&&(f=f[t]))return f;if(n!==xe&&de(n,t))return l[t]=4,n[t];if(h=a.config.globalProperties,de(h,t))return h[t]},set({_:e},t,n){const{data:r,setupState:o,ctx:s}=e;return zr(o,t)?(o[t]=n,!0):r!==xe&&de(r,t)?(r[t]=n,!0):de(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(s[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:r,appContext:o,propsOptions:s}},l){let i;return!!n[l]||e!==xe&&de(e,l)||zr(t,l)||(i=s[0])&&de(i,l)||de(r,l)||de(Cn,l)||de(o.config.globalProperties,l)},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:de(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function cs(e){return Q(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let lo=!0;function Sc(e){const t=Oo(e),n=e.proxy,r=e.ctx;lo=!1,t.beforeCreate&&us(t.beforeCreate,e,"bc");const{data:o,computed:s,methods:l,watch:i,provide:a,inject:c,created:u,beforeMount:f,mounted:h,beforeUpdate:g,updated:y,activated:w,deactivated:C,beforeDestroy:v,beforeUnmount:b,destroyed:P,unmounted:S,render:U,renderTracked:Z,renderTriggered:$,errorCaptured:m,serverPrefetch:z,expose:M,inheritAttrs:G,components:L,directives:R,filters:I}=t;if(c&&Pc(c,r,null),l)for(const re in l){const se=l[re];oe(se)&&(r[re]=se.bind(n))}if(o){const re=o.call(n,n);ke(re)&&(e.data=vn(re))}if(lo=!0,s)for(const re in s){const se=s[re],He=oe(se)?se.bind(n,n):oe(se.get)?se.get.bind(n,n):ot,Ne=!oe(se)&&oe(se.set)?se.set.bind(n):ot,Ve=F({get:He,set:Ne});Object.defineProperty(r,re,{enumerable:!0,configurable:!0,get:()=>Ve.value,set:Fe=>Ve.value=Fe})}if(i)for(const re in i)Jl(i[re],r,n,re);if(a){const re=oe(a)?a.call(n):a;Reflect.ownKeys(re).forEach(se=>{Wt(se,re[se])})}u&&us(u,e,"c");function V(re,se){Q(se)?se.forEach(He=>re(He.bind(n))):se&&re(se.bind(n))}if(V(_c,f),V(Je,h),V(bc,g),V(yc,y),V(mc,w),V(gc,C),V(xc,m),V(kc,Z),V(wc,$),V(Un,b),V(Rr,S),V(Ec,z),Q(M))if(M.length){const re=e.exposed||(e.exposed={});M.forEach(se=>{Object.defineProperty(re,se,{get:()=>n[se],set:He=>n[se]=He})})}else e.exposed||(e.exposed={});U&&e.render===ot&&(e.render=U),G!=null&&(e.inheritAttrs=G),L&&(e.components=L),R&&(e.directives=R)}function Pc(e,t,n=ot){Q(e)&&(e=io(e));for(const r in e){const o=e[r];let s;ke(o)?"default"in o?s=Pe(o.from||r,o.default,!0):s=Pe(o.from||r):s=Pe(o),Ie(s)?Object.defineProperty(t,r,{enumerable:!0,configurable:!0,get:()=>s.value,set:l=>s.value=l}):t[r]=s}}function us(e,t,n){Xe(Q(e)?e.map(r=>r.bind(t.proxy)):e.bind(t.proxy),t,n)}function Jl(e,t,n,r){const o=r.includes(".")?Vl(n,r):()=>n[r];if(ge(e)){const s=t[e];oe(s)&&st(o,s)}else if(oe(e))st(o,e.bind(n));else if(ke(e))if(Q(e))e.forEach(s=>Jl(s,t,n,r));else{const s=oe(e.handler)?e.handler.bind(n):t[e.handler];oe(s)&&st(o,s,e)}}function Oo(e){const t=e.type,{mixins:n,extends:r}=t,{mixins:o,optionsCache:s,config:{optionMergeStrategies:l}}=e.appContext,i=s.get(t);let a;return i?a=i:!o.length&&!n&&!r?a=t:(a={},o.length&&o.forEach(c=>mr(a,c,l,!0)),mr(a,t,l)),ke(t)&&s.set(t,a),a}function mr(e,t,n,r=!1){const{mixins:o,extends:s}=t;s&&mr(e,s,n,!0),o&&o.forEach(l=>mr(e,l,n,!0));for(const l in t)if(!(r&&l==="expose")){const i=Ac[l]||n&&n[l];e[l]=i?i(e[l],t[l]):t[l]}return e}const Ac={data:fs,props:ds,emits:ds,methods:xn,computed:xn,beforeCreate:Be,created:Be,beforeMount:Be,mounted:Be,beforeUpdate:Be,updated:Be,beforeDestroy:Be,beforeUnmount:Be,destroyed:Be,unmounted:Be,activated:Be,deactivated:Be,errorCaptured:Be,serverPrefetch:Be,components:xn,directives:xn,watch:Rc,provide:fs,inject:Oc};function fs(e,t){return t?e?function(){return Ae(oe(e)?e.call(this,this):e,oe(t)?t.call(this,this):t)}:t:e}function Oc(e,t){return xn(io(e),io(t))}function io(e){if(Q(e)){const t={};for(let n=0;n1)return n&&oe(t)?t.call(r&&r.proxy):t}}function Nc(e,t,n,r=!1){const o={},s={};cr(s,Ir,1),e.propsDefaults=Object.create(null),Zl(e,t,o,s);for(const l in e.propsOptions[0])l in o||(o[l]=void 0);n?e.props=r?o:Va(o):e.type.props?e.props=o:e.props=s,e.attrs=s}function Dc(e,t,n,r){const{props:o,attrs:s,vnode:{patchFlag:l}}=e,i=pe(o),[a]=e.propsOptions;let c=!1;if((r||l>0)&&!(l&16)){if(l&8){const u=e.vnode.dynamicProps;for(let f=0;f{a=!0;const[h,g]=Xl(f,t,!0);Ae(l,h),g&&i.push(...g)};!n&&t.mixins.length&&t.mixins.forEach(u),e.extends&&u(e.extends),e.mixins&&e.mixins.forEach(u)}if(!s&&!a)return ke(e)&&r.set(e,rn),rn;if(Q(s))for(let u=0;u-1,g[1]=w<0||y-1||de(g,"default"))&&i.push(f)}}}const c=[l,i];return ke(e)&&r.set(e,c),c}function hs(e){return e[0]!=="$"}function ps(e){const t=e&&e.toString().match(/^\s*(function|class) (\w+)/);return t?t[2]:e===null?"null":""}function ms(e,t){return ps(e)===ps(t)}function gs(e,t){return Q(t)?t.findIndex(n=>ms(n,e)):oe(t)&&ms(t,e)?0:-1}const ei=e=>e[0]==="_"||e==="$stable",Ro=e=>Q(e)?e.map(tt):[tt(e)],Mc=(e,t,n)=>{if(t._n)return t;const r=De((...o)=>Ro(t(...o)),n);return r._c=!1,r},ti=(e,t,n)=>{const r=e._ctx;for(const o in e){if(ei(o))continue;const s=e[o];if(oe(s))t[o]=Mc(o,s,r);else if(s!=null){const l=Ro(s);t[o]=()=>l}}},ni=(e,t)=>{const n=Ro(t);e.slots.default=()=>n},Hc=(e,t)=>{if(e.vnode.shapeFlag&32){const n=t._;n?(e.slots=pe(t),cr(t,"_",n)):ti(t,e.slots={})}else e.slots={},t&&ni(e,t);cr(e.slots,Ir,1)},Fc=(e,t,n)=>{const{vnode:r,slots:o}=e;let s=!0,l=xe;if(r.shapeFlag&32){const i=t._;i?n&&i===1?s=!1:(Ae(o,t),!n&&i===1&&delete o._):(s=!t.$stable,ti(t,o)),l=t}else t&&(ni(e,t),l={default:1});if(s)for(const i in o)!ei(i)&&!(i in l)&&delete o[i]};function vr(e,t,n,r,o=!1){if(Q(e)){e.forEach((h,g)=>vr(h,t&&(Q(t)?t[g]:t),n,r,o));return}if(an(r)&&!o)return;const s=r.shapeFlag&4?$r(r.component)||r.component.proxy:r.el,l=o?null:s,{i,r:a}=e,c=t&&t.r,u=i.refs===xe?i.refs={}:i.refs,f=i.setupState;if(c!=null&&c!==a&&(ge(c)?(u[c]=null,de(f,c)&&(f[c]=null)):Ie(c)&&(c.value=null)),oe(a))Ot(a,i,12,[l,u]);else{const h=ge(a),g=Ie(a);if(h||g){const y=()=>{if(e.f){const w=h?de(f,a)?f[a]:u[a]:a.value;o?Q(w)&&vo(w,s):Q(w)?w.includes(s)||w.push(s):h?(u[a]=[s],de(f,a)&&(f[a]=u[a])):(a.value=[s],e.k&&(u[e.k]=a.value))}else h?(u[a]=l,de(f,a)&&(f[a]=l)):g&&(a.value=l,e.k&&(u[e.k]=l))};l?(y.id=-1,Ue(y,n)):y()}}}let xt=!1;const er=e=>/svg/.test(e.namespaceURI)&&e.tagName!=="foreignObject",tr=e=>e.nodeType===8;function jc(e){const{mt:t,p:n,o:{patchProp:r,createText:o,nextSibling:s,parentNode:l,remove:i,insert:a,createComment:c}}=e,u=(v,b)=>{if(!b.hasChildNodes()){n(null,v,b),dr(),b._vnode=v;return}xt=!1,f(b.firstChild,v,null,null,null),dr(),b._vnode=v,xt&&console.error("Hydration completed but contains mismatches.")},f=(v,b,P,S,U,Z=!1)=>{const $=tr(v)&&v.data==="[",m=()=>w(v,b,P,S,U,$),{type:z,ref:M,shapeFlag:G,patchFlag:L}=b;let R=v.nodeType;b.el=v,L===-2&&(Z=!1,b.dynamicChildren=null);let I=null;switch(z){case un:R!==3?b.children===""?(a(b.el=o(""),l(v),v),I=v):I=m():(v.data!==b.children&&(xt=!0,v.data=b.children),I=s(v));break;case Ye:R!==8||$?I=m():I=s(v);break;case Tn:if($&&(v=s(v),R=v.nodeType),R===1||R===3){I=v;const le=!b.children.length;for(let V=0;V{Z=Z||!!b.dynamicChildren;const{type:$,props:m,patchFlag:z,shapeFlag:M,dirs:G}=b,L=$==="input"&&G||$==="option";if(L||z!==-1){if(G&&ct(b,null,P,"created"),m)if(L||!Z||z&48)for(const I in m)(L&&I.endsWith("value")||Fn(I)&&!Ln(I))&&r(v,I,null,m[I],!1,void 0,P);else m.onClick&&r(v,"onClick",null,m.onClick,!1,void 0,P);let R;if((R=m&&m.onVnodeBeforeMount)&&Ze(R,P,b),G&&ct(b,null,P,"beforeMount"),((R=m&&m.onVnodeMounted)||G)&&zl(()=>{R&&Ze(R,P,b),G&&ct(b,null,P,"mounted")},S),M&16&&!(m&&(m.innerHTML||m.textContent))){let I=g(v.firstChild,b,v,P,S,U,Z);for(;I;){xt=!0;const le=I;I=I.nextSibling,i(le)}}else M&8&&v.textContent!==b.children&&(xt=!0,v.textContent=b.children)}return v.nextSibling},g=(v,b,P,S,U,Z,$)=>{$=$||!!b.dynamicChildren;const m=b.children,z=m.length;for(let M=0;M{const{slotScopeIds:$}=b;$&&(U=U?U.concat($):$);const m=l(v),z=g(s(v),b,m,P,S,U,Z);return z&&tr(z)&&z.data==="]"?s(b.anchor=z):(xt=!0,a(b.anchor=c("]"),m,z),z)},w=(v,b,P,S,U,Z)=>{if(xt=!0,b.el=null,Z){const z=C(v);for(;;){const M=s(v);if(M&&M!==z)i(M);else break}}const $=s(v),m=l(v);return i(v),n(null,b,m,$,P,S,er(m),U),$},C=v=>{let b=0;for(;v;)if(v=s(v),v&&tr(v)&&(v.data==="["&&b++,v.data==="]")){if(b===0)return s(v);b--}return v};return[u,f]}const Ue=zl;function Bc(e){return zc(e,jc)}function zc(e,t){const n=Zr();n.__VUE__=!0;const{insert:r,remove:o,patchProp:s,createElement:l,createText:i,createComment:a,setText:c,setElementText:u,parentNode:f,nextSibling:h,setScopeId:g=ot,insertStaticContent:y}=e,w=(d,p,_,E=null,x=null,T=null,H=!1,O=null,D=!!p.dynamicChildren)=>{if(d===p)return;d&&!zt(d,p)&&(E=k(d),Fe(d,x,T,!0),d=null),p.patchFlag===-2&&(D=!1,p.dynamicChildren=null);const{type:A,ref:Y,shapeFlag:q}=p;switch(A){case un:C(d,p,_,E);break;case Ye:v(d,p,_,E);break;case Tn:d==null&&b(p,_,E,H);break;case we:L(d,p,_,E,x,T,H,O,D);break;default:q&1?U(d,p,_,E,x,T,H,O,D):q&6?R(d,p,_,E,x,T,H,O,D):(q&64||q&128)&&A.process(d,p,_,E,x,T,H,O,D,N)}Y!=null&&x&&vr(Y,d&&d.ref,T,p||d,!p)},C=(d,p,_,E)=>{if(d==null)r(p.el=i(p.children),_,E);else{const x=p.el=d.el;p.children!==d.children&&c(x,p.children)}},v=(d,p,_,E)=>{d==null?r(p.el=a(p.children||""),_,E):p.el=d.el},b=(d,p,_,E)=>{[d.el,d.anchor]=y(d.children,p,_,E,d.el,d.anchor)},P=({el:d,anchor:p},_,E)=>{let x;for(;d&&d!==p;)x=h(d),r(d,_,E),d=x;r(p,_,E)},S=({el:d,anchor:p})=>{let _;for(;d&&d!==p;)_=h(d),o(d),d=_;o(p)},U=(d,p,_,E,x,T,H,O,D)=>{H=H||p.type==="svg",d==null?Z(p,_,E,x,T,H,O,D):z(d,p,x,T,H,O,D)},Z=(d,p,_,E,x,T,H,O)=>{let D,A;const{type:Y,props:q,shapeFlag:J,transition:ne,dirs:ie}=d;if(D=d.el=l(d.type,T,q&&q.is,q),J&8?u(D,d.children):J&16&&m(d.children,D,null,E,x,T&&Y!=="foreignObject",H,O),ie&&ct(d,null,E,"created"),$(D,d,d.scopeId,H,E),q){for(const be in q)be!=="value"&&!Ln(be)&&s(D,be,null,q[be],T,d.children,E,x,$e);"value"in q&&s(D,"value",null,q.value),(A=q.onVnodeBeforeMount)&&Ze(A,E,d)}ie&&ct(d,null,E,"beforeMount");const ye=(!x||x&&!x.pendingBranch)&&ne&&!ne.persisted;ye&&ne.beforeEnter(D),r(D,p,_),((A=q&&q.onVnodeMounted)||ye||ie)&&Ue(()=>{A&&Ze(A,E,d),ye&&ne.enter(D),ie&&ct(d,null,E,"mounted")},x)},$=(d,p,_,E,x)=>{if(_&&g(d,_),E)for(let T=0;T{for(let A=D;A{const O=p.el=d.el;let{patchFlag:D,dynamicChildren:A,dirs:Y}=p;D|=d.patchFlag&16;const q=d.props||xe,J=p.props||xe;let ne;_&&Mt(_,!1),(ne=J.onVnodeBeforeUpdate)&&Ze(ne,_,p,d),Y&&ct(p,d,_,"beforeUpdate"),_&&Mt(_,!0);const ie=x&&p.type!=="foreignObject";if(A?M(d.dynamicChildren,A,O,_,E,ie,T):H||se(d,p,O,null,_,E,ie,T,!1),D>0){if(D&16)G(O,p,q,J,_,E,x);else if(D&2&&q.class!==J.class&&s(O,"class",null,J.class,x),D&4&&s(O,"style",q.style,J.style,x),D&8){const ye=p.dynamicProps;for(let be=0;be{ne&&Ze(ne,_,p,d),Y&&ct(p,d,_,"updated")},E)},M=(d,p,_,E,x,T,H)=>{for(let O=0;O{if(_!==E){if(_!==xe)for(const O in _)!Ln(O)&&!(O in E)&&s(d,O,_[O],null,H,p.children,x,T,$e);for(const O in E){if(Ln(O))continue;const D=E[O],A=_[O];D!==A&&O!=="value"&&s(d,O,A,D,H,p.children,x,T,$e)}"value"in E&&s(d,"value",_.value,E.value)}},L=(d,p,_,E,x,T,H,O,D)=>{const A=p.el=d?d.el:i(""),Y=p.anchor=d?d.anchor:i("");let{patchFlag:q,dynamicChildren:J,slotScopeIds:ne}=p;ne&&(O=O?O.concat(ne):ne),d==null?(r(A,_,E),r(Y,_,E),m(p.children,_,Y,x,T,H,O,D)):q>0&&q&64&&J&&d.dynamicChildren?(M(d.dynamicChildren,J,_,x,T,H,O),(p.key!=null||x&&p===x.subTree)&&ri(d,p,!0)):se(d,p,_,Y,x,T,H,O,D)},R=(d,p,_,E,x,T,H,O,D)=>{p.slotScopeIds=O,d==null?p.shapeFlag&512?x.ctx.activate(p,_,E,H,D):I(p,_,E,x,T,H,D):le(d,p,D)},I=(d,p,_,E,x,T,H)=>{const O=d.component=Qc(d,E,x);if(Vn(d)&&(O.ctx.renderer=N),Zc(O),O.asyncDep){if(x&&x.registerDep(O,V),!d.el){const D=O.subTree=te(Ye);v(null,D,p,_)}return}V(O,d,p,_,x,T,H)},le=(d,p,_)=>{const E=p.component=d.component;if(ic(d,p,_))if(E.asyncDep&&!E.asyncResolved){re(E,p,_);return}else E.next=p,tc(E.update),E.update();else p.el=d.el,E.vnode=p},V=(d,p,_,E,x,T,H)=>{const O=()=>{if(d.isMounted){let{next:Y,bu:q,u:J,parent:ne,vnode:ie}=d,ye=Y,be;Mt(d,!1),Y?(Y.el=ie.el,re(d,Y,H)):Y=ie,q&&Hr(q),(be=Y.props&&Y.props.onVnodeBeforeUpdate)&&Ze(be,ne,Y,ie),Mt(d,!0);const Te=Fr(d),et=d.subTree;d.subTree=Te,w(et,Te,f(et.el),k(et),d,x,T),Y.el=Te.el,ye===null&&ac(d,Te.el),J&&Ue(J,x),(be=Y.props&&Y.props.onVnodeUpdated)&&Ue(()=>Ze(be,ne,Y,ie),x)}else{let Y;const{el:q,props:J}=p,{bm:ne,m:ie,parent:ye}=d,be=an(p);if(Mt(d,!1),ne&&Hr(ne),!be&&(Y=J&&J.onVnodeBeforeMount)&&Ze(Y,ye,p),Mt(d,!0),q&&ce){const Te=()=>{d.subTree=Fr(d),ce(q,d.subTree,d,x,null)};be?p.type.__asyncLoader().then(()=>!d.isUnmounted&&Te()):Te()}else{const Te=d.subTree=Fr(d);w(null,Te,_,E,d,x,T),p.el=Te.el}if(ie&&Ue(ie,x),!be&&(Y=J&&J.onVnodeMounted)){const Te=p;Ue(()=>Ze(Y,ye,Te),x)}(p.shapeFlag&256||ye&&an(ye.vnode)&&ye.vnode.shapeFlag&256)&&d.a&&Ue(d.a,x),d.isMounted=!0,p=_=E=null}},D=d.effect=new Eo(O,()=>Pr(A),d.scope),A=d.update=()=>D.run();A.id=d.uid,Mt(d,!0),A()},re=(d,p,_)=>{p.component=d;const E=d.vnode.props;d.vnode=p,d.next=null,Dc(d,p.props,E,_),Fc(d,p.children,_),mn(),ss(),gn()},se=(d,p,_,E,x,T,H,O,D=!1)=>{const A=d&&d.children,Y=d?d.shapeFlag:0,q=p.children,{patchFlag:J,shapeFlag:ne}=p;if(J>0){if(J&128){Ne(A,q,_,E,x,T,H,O,D);return}else if(J&256){He(A,q,_,E,x,T,H,O,D);return}}ne&8?(Y&16&&$e(A,x,T),q!==A&&u(_,q)):Y&16?ne&16?Ne(A,q,_,E,x,T,H,O,D):$e(A,x,T,!0):(Y&8&&u(_,""),ne&16&&m(q,_,E,x,T,H,O,D))},He=(d,p,_,E,x,T,H,O,D)=>{d=d||rn,p=p||rn;const A=d.length,Y=p.length,q=Math.min(A,Y);let J;for(J=0;JY?$e(d,x,T,!0,!1,q):m(p,_,E,x,T,H,O,D,q)},Ne=(d,p,_,E,x,T,H,O,D)=>{let A=0;const Y=p.length;let q=d.length-1,J=Y-1;for(;A<=q&&A<=J;){const ne=d[A],ie=p[A]=D?Tt(p[A]):tt(p[A]);if(zt(ne,ie))w(ne,ie,_,null,x,T,H,O,D);else break;A++}for(;A<=q&&A<=J;){const ne=d[q],ie=p[J]=D?Tt(p[J]):tt(p[J]);if(zt(ne,ie))w(ne,ie,_,null,x,T,H,O,D);else break;q--,J--}if(A>q){if(A<=J){const ne=J+1,ie=neJ)for(;A<=q;)Fe(d[A],x,T,!0),A++;else{const ne=A,ie=A,ye=new Map;for(A=ie;A<=J;A++){const Ke=p[A]=D?Tt(p[A]):tt(p[A]);Ke.key!=null&&ye.set(Ke.key,A)}let be,Te=0;const et=J-ie+1;let Zt=!1,Ko=0;const _n=new Array(et);for(A=0;A=et){Fe(Ke,x,T,!0);continue}let at;if(Ke.key!=null)at=ye.get(Ke.key);else for(be=ie;be<=J;be++)if(_n[be-ie]===0&&zt(Ke,p[be])){at=be;break}at===void 0?Fe(Ke,x,T,!0):(_n[at-ie]=A+1,at>=Ko?Ko=at:Zt=!0,w(Ke,p[at],_,null,x,T,H,O,D),Te++)}const Go=Zt?Vc(_n):rn;for(be=Go.length-1,A=et-1;A>=0;A--){const Ke=ie+A,at=p[Ke],Yo=Ke+1{const{el:T,type:H,transition:O,children:D,shapeFlag:A}=d;if(A&6){Ve(d.component.subTree,p,_,E);return}if(A&128){d.suspense.move(p,_,E);return}if(A&64){H.move(d,p,_,N);return}if(H===we){r(T,p,_);for(let q=0;qO.enter(T),x);else{const{leave:q,delayLeave:J,afterLeave:ne}=O,ie=()=>r(T,p,_),ye=()=>{q(T,()=>{ie(),ne&&ne()})};J?J(T,ie,ye):ye()}else r(T,p,_)},Fe=(d,p,_,E=!1,x=!1)=>{const{type:T,props:H,ref:O,children:D,dynamicChildren:A,shapeFlag:Y,patchFlag:q,dirs:J}=d;if(O!=null&&vr(O,null,_,d,!0),Y&256){p.ctx.deactivate(d);return}const ne=Y&1&&J,ie=!an(d);let ye;if(ie&&(ye=H&&H.onVnodeBeforeUnmount)&&Ze(ye,p,d),Y&6)it(d.component,_,E);else{if(Y&128){d.suspense.unmount(_,E);return}ne&&ct(d,null,p,"beforeUnmount"),Y&64?d.type.remove(d,p,_,x,N,E):A&&(T!==we||q>0&&q&64)?$e(A,p,_,!1,!0):(T===we&&q&384||!x&&Y&16)&&$e(D,p,_),E&&Et(d)}(ie&&(ye=H&&H.onVnodeUnmounted)||ne)&&Ue(()=>{ye&&Ze(ye,p,d),ne&&ct(d,null,p,"unmounted")},_)},Et=d=>{const{type:p,el:_,anchor:E,transition:x}=d;if(p===we){wt(_,E);return}if(p===Tn){S(d);return}const T=()=>{o(_),x&&!x.persisted&&x.afterLeave&&x.afterLeave()};if(d.shapeFlag&1&&x&&!x.persisted){const{leave:H,delayLeave:O}=x,D=()=>H(_,T);O?O(d.el,T,D):D()}else T()},wt=(d,p)=>{let _;for(;d!==p;)_=h(d),o(d),d=_;o(p)},it=(d,p,_)=>{const{bum:E,scope:x,update:T,subTree:H,um:O}=d;E&&Hr(E),x.stop(),T&&(T.active=!1,Fe(H,d,p,_)),O&&Ue(O,p),Ue(()=>{d.isUnmounted=!0},p),p&&p.pendingBranch&&!p.isUnmounted&&d.asyncDep&&!d.asyncResolved&&d.suspenseId===p.pendingId&&(p.deps--,p.deps===0&&p.resolve())},$e=(d,p,_,E=!1,x=!1,T=0)=>{for(let H=T;Hd.shapeFlag&6?k(d.component.subTree):d.shapeFlag&128?d.suspense.next():h(d.anchor||d.el),B=(d,p,_)=>{d==null?p._vnode&&Fe(p._vnode,null,null,!0):w(p._vnode||null,d,p,null,null,null,_),ss(),dr(),p._vnode=d},N={p:w,um:Fe,m:Ve,r:Et,mt:I,mc:m,pc:se,pbc:M,n:k,o:e};let K,ce;return t&&([K,ce]=t(N)),{render:B,hydrate:K,createApp:$c(B,K)}}function Mt({effect:e,update:t},n){e.allowRecurse=t.allowRecurse=n}function ri(e,t,n=!1){const r=e.children,o=t.children;if(Q(r)&&Q(o))for(let s=0;s>1,e[n[i]]0&&(t[r]=n[s-1]),n[s]=r)}}for(s=n.length,l=n[s-1];s-- >0;)n[s]=l,l=t[l];return n}const Uc=e=>e.__isTeleport,we=Symbol.for("v-fgt"),un=Symbol.for("v-txt"),Ye=Symbol.for("v-cmt"),Tn=Symbol.for("v-stc"),Sn=[];let rt=null;function j(e=!1){Sn.push(rt=e?null:[])}function qc(){Sn.pop(),rt=Sn[Sn.length-1]||null}let Nn=1;function vs(e){Nn+=e}function oi(e){return e.dynamicChildren=Nn>0?rt||rn:null,qc(),Nn>0&&rt&&rt.push(e),e}function X(e,t,n,r,o,s){return oi(he(e,t,n,r,o,s,!0))}function Se(e,t,n,r,o){return oi(te(e,t,n,r,o,!0))}function _r(e){return e?e.__v_isVNode===!0:!1}function zt(e,t){return e.type===t.type&&e.key===t.key}const Ir="__vInternal",si=({key:e})=>e??null,ir=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?ge(e)||Ie(e)||oe(e)?{i:Me,r:e,k:t,f:!!n}:e:null);function he(e,t=null,n=null,r=0,o=null,s=e===we?0:1,l=!1,i=!1){const a={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&si(t),ref:t&&ir(t),scopeId:Bl,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetAnchor:null,staticCount:0,shapeFlag:s,patchFlag:r,dynamicProps:o,dynamicChildren:null,appContext:null,ctx:Me};return i?(Io(a,n),s&128&&e.normalize(a)):n&&(a.shapeFlag|=ge(n)?8:16),Nn>0&&!l&&rt&&(a.patchFlag>0||s&6)&&a.patchFlag!==32&&rt.push(a),a}const te=Wc;function Wc(e,t=null,n=null,r=0,o=null,s=!1){if((!e||e===Lc)&&(e=Ye),_r(e)){const i=$t(e,t,!0);return n&&Io(i,n),Nn>0&&!s&&rt&&(i.shapeFlag&6?rt[rt.indexOf(e)]=i:rt.push(i)),i.patchFlag|=-2,i}if(ru(e)&&(e=e.__vccOpts),t){t=Kc(t);let{class:i,style:a}=t;i&&!ge(i)&&(t.class=qe(i)),ke(a)&&(Ol(a)&&!Q(a)&&(a=Ae({},a)),t.style=jn(a))}const l=ge(e)?1:cc(e)?128:Uc(e)?64:ke(e)?4:oe(e)?2:0;return he(e,t,n,r,o,l,s,!0)}function Kc(e){return e?Ol(e)||Ir in e?Ae({},e):e:null}function $t(e,t,n=!1){const{props:r,ref:o,patchFlag:s,children:l}=e,i=t?co(r||{},t):r;return{__v_isVNode:!0,__v_skip:!0,type:e.type,props:i,key:i&&si(i),ref:t&&t.ref?n&&o?Q(o)?o.concat(ir(t)):[o,ir(t)]:ir(t):o,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:l,target:e.target,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==we?s===-1?16:s|16:s,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:e.transition,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&$t(e.ssContent),ssFallback:e.ssFallback&&$t(e.ssFallback),el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce}}function Nt(e=" ",t=0){return te(un,null,e,t)}function Gc(e,t){const n=te(Tn,null,e);return n.staticCount=t,n}function Ce(e="",t=!1){return t?(j(),Se(Ye,null,e)):te(Ye,null,e)}function tt(e){return e==null||typeof e=="boolean"?te(Ye):Q(e)?te(we,null,e.slice()):typeof e=="object"?Tt(e):te(un,null,String(e))}function Tt(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:$t(e)}function Io(e,t){let n=0;const{shapeFlag:r}=e;if(t==null)t=null;else if(Q(t))n=16;else if(typeof t=="object")if(r&65){const o=t.default;o&&(o._c&&(o._d=!1),Io(e,o()),o._c&&(o._d=!0));return}else{n=32;const o=t._;!o&&!(Ir in t)?t._ctx=Me:o===3&&Me&&(Me.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else oe(t)?(t={default:t,_ctx:Me},n=32):(t=String(t),r&64?(n=16,t=[Nt(t)]):n=8);e.children=t,e.shapeFlag|=n}function co(...e){const t={};for(let n=0;nOe||Me;let $o,Xt,_s="__VUE_INSTANCE_SETTERS__";(Xt=Zr()[_s])||(Xt=Zr()[_s]=[]),Xt.push(e=>Oe=e),$o=e=>{Xt.length>1?Xt.forEach(t=>t(e)):Xt[0](e)};const fn=e=>{$o(e),e.scope.on()},Kt=()=>{Oe&&Oe.scope.off(),$o(null)};function ii(e){return e.vnode.shapeFlag&4}let dn=!1;function Zc(e,t=!1){dn=t;const{props:n,children:r}=e.vnode,o=ii(e);Nc(e,n,o,t),Hc(e,r);const s=o?Xc(e,t):void 0;return dn=!1,s}function Xc(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=Rl(new Proxy(e.ctx,Tc));const{setup:r}=n;if(r){const o=e.setupContext=r.length>1?tu(e):null;fn(e),mn();const s=Ot(r,e,0,[e.props,o]);if(gn(),Kt(),ml(s)){if(s.then(Kt,Kt),t)return s.then(l=>{bs(e,l,t)}).catch(l=>{zn(l,e,0)});e.asyncDep=s}else bs(e,s,t)}else ai(e,t)}function bs(e,t,n){oe(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:ke(t)&&(e.setupState=Nl(t)),ai(e,n)}let ys;function ai(e,t,n){const r=e.type;if(!e.render){if(!t&&ys&&!r.render){const o=r.template||Oo(e).template;if(o){const{isCustomElement:s,compilerOptions:l}=e.appContext.config,{delimiters:i,compilerOptions:a}=r,c=Ae(Ae({isCustomElement:s,delimiters:i},l),a);r.render=ys(o,c)}}e.render=r.render||ot}fn(e),mn(),Sc(e),gn(),Kt()}function eu(e){return e.attrsProxy||(e.attrsProxy=new Proxy(e.attrs,{get(t,n){return We(e,"get","$attrs"),t[n]}}))}function tu(e){const t=n=>{e.exposed=n||{}};return{get attrs(){return eu(e)},slots:e.slots,emit:e.emit,expose:t}}function $r(e){if(e.exposed)return e.exposeProxy||(e.exposeProxy=new Proxy(Nl(Rl(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in Cn)return Cn[n](e)},has(t,n){return n in t||n in Cn}}))}function nu(e,t=!0){return oe(e)?e.displayName||e.name:e.name||t&&e.__name}function ru(e){return oe(e)&&"__vccOpts"in e}const F=(e,t)=>Za(e,t,dn);function ae(e,t,n){const r=arguments.length;return r===2?ke(t)&&!Q(t)?_r(t)?te(e,null,[t]):te(e,t):te(e,null,t):(r>3?n=Array.prototype.slice.call(arguments,2):r===3&&_r(n)&&(n=[n]),te(e,t,n))}const ou=Symbol.for("v-scx"),su=()=>Pe(ou),lu="3.3.4",iu="http://www.w3.org/2000/svg",Vt=typeof document<"u"?document:null,Es=Vt&&Vt.createElement("template"),au={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,r)=>{const o=t?Vt.createElementNS(iu,e):Vt.createElement(e,n?{is:n}:void 0);return e==="select"&&r&&r.multiple!=null&&o.setAttribute("multiple",r.multiple),o},createText:e=>Vt.createTextNode(e),createComment:e=>Vt.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Vt.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,r,o,s){const l=n?n.previousSibling:t.lastChild;if(o&&(o===s||o.nextSibling))for(;t.insertBefore(o.cloneNode(!0),n),!(o===s||!(o=o.nextSibling)););else{Es.innerHTML=r?`${e}`:e;const i=Es.content;if(r){const a=i.firstChild;for(;a.firstChild;)i.appendChild(a.firstChild);i.removeChild(a)}t.insertBefore(i,n)}return[l?l.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}};function cu(e,t,n){const r=e._vtc;r&&(t=(t?[t,...r]:[...r]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}function uu(e,t,n){const r=e.style,o=ge(n);if(n&&!o){if(t&&!ge(t))for(const s in t)n[s]==null&&uo(r,s,"");for(const s in n)uo(r,s,n[s])}else{const s=r.display;o?t!==n&&(r.cssText=n):t&&e.removeAttribute("style"),"_vod"in e&&(r.display=s)}}const ws=/\s*!important$/;function uo(e,t,n){if(Q(n))n.forEach(r=>uo(e,t,r));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const r=fu(e,t);ws.test(n)?e.setProperty(Yt(r),n.replace(ws,""),"important"):e[r]=n}}const ks=["Webkit","Moz","ms"],Vr={};function fu(e,t){const n=Vr[t];if(n)return n;let r=ft(t);if(r!=="filter"&&r in e)return Vr[t]=r;r=Lr(r);for(let o=0;oUr||(_u.then(()=>Ur=0),Ur=Date.now());function yu(e,t){const n=r=>{if(!r._vts)r._vts=Date.now();else if(r._vts<=n.attached)return;Xe(Eu(r,n.value),t,5,[r])};return n.value=e,n.attached=bu(),n}function Eu(e,t){if(Q(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(r=>o=>!o._stopped&&r&&r(o))}else return t}const Cs=/^on[a-z]/,wu=(e,t,n,r,o=!1,s,l,i,a)=>{t==="class"?cu(e,r,o):t==="style"?uu(e,n,r):Fn(t)?go(t)||gu(e,t,n,r,l):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):ku(e,t,r,o))?hu(e,t,r,s,l,i,a):(t==="true-value"?e._trueValue=r:t==="false-value"&&(e._falseValue=r),du(e,t,r,o))};function ku(e,t,n,r){return r?!!(t==="innerHTML"||t==="textContent"||t in e&&Cs.test(t)&&oe(n)):t==="spellcheck"||t==="draggable"||t==="translate"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA"||Cs.test(t)&&ge(n)?!1:t in e}const Lt="transition",bn="animation",qn=(e,{slots:t})=>ae(pc,xu(e),t);qn.displayName="Transition";const ci={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String};qn.props=Ae({},Ul,ci);const Ht=(e,t=[])=>{Q(e)?e.forEach(n=>n(...t)):e&&e(...t)},Ts=e=>e?Q(e)?e.some(t=>t.length>1):e.length>1:!1;function xu(e){const t={};for(const L in e)L in ci||(t[L]=e[L]);if(e.css===!1)return t;const{name:n="v",type:r,duration:o,enterFromClass:s=`${n}-enter-from`,enterActiveClass:l=`${n}-enter-active`,enterToClass:i=`${n}-enter-to`,appearFromClass:a=s,appearActiveClass:c=l,appearToClass:u=i,leaveFromClass:f=`${n}-leave-from`,leaveActiveClass:h=`${n}-leave-active`,leaveToClass:g=`${n}-leave-to`}=e,y=Lu(o),w=y&&y[0],C=y&&y[1],{onBeforeEnter:v,onEnter:b,onEnterCancelled:P,onLeave:S,onLeaveCancelled:U,onBeforeAppear:Z=v,onAppear:$=b,onAppearCancelled:m=P}=t,z=(L,R,I)=>{Ft(L,R?u:i),Ft(L,R?c:l),I&&I()},M=(L,R)=>{L._isLeaving=!1,Ft(L,f),Ft(L,g),Ft(L,h),R&&R()},G=L=>(R,I)=>{const le=L?$:b,V=()=>z(R,L,I);Ht(le,[R,V]),Ss(()=>{Ft(R,L?a:s),Ct(R,L?u:i),Ts(le)||Ps(R,r,w,V)})};return Ae(t,{onBeforeEnter(L){Ht(v,[L]),Ct(L,s),Ct(L,l)},onBeforeAppear(L){Ht(Z,[L]),Ct(L,a),Ct(L,c)},onEnter:G(!1),onAppear:G(!0),onLeave(L,R){L._isLeaving=!0;const I=()=>M(L,R);Ct(L,f),Su(),Ct(L,h),Ss(()=>{L._isLeaving&&(Ft(L,f),Ct(L,g),Ts(S)||Ps(L,r,C,I))}),Ht(S,[L,I])},onEnterCancelled(L){z(L,!1),Ht(P,[L])},onAppearCancelled(L){z(L,!0),Ht(m,[L])},onLeaveCancelled(L){M(L),Ht(U,[L])}})}function Lu(e){if(e==null)return null;if(ke(e))return[qr(e.enter),qr(e.leave)];{const t=qr(e);return[t,t]}}function qr(e){return la(e)}function Ct(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e._vtc||(e._vtc=new Set)).add(t)}function Ft(e,t){t.split(/\s+/).forEach(r=>r&&e.classList.remove(r));const{_vtc:n}=e;n&&(n.delete(t),n.size||(e._vtc=void 0))}function Ss(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let Cu=0;function Ps(e,t,n,r){const o=e._endId=++Cu,s=()=>{o===e._endId&&r()};if(n)return setTimeout(s,n);const{type:l,timeout:i,propCount:a}=Tu(e,t);if(!l)return r();const c=l+"end";let u=0;const f=()=>{e.removeEventListener(c,h),s()},h=g=>{g.target===e&&++u>=a&&f()};setTimeout(()=>{u(n[y]||"").split(", "),o=r(`${Lt}Delay`),s=r(`${Lt}Duration`),l=As(o,s),i=r(`${bn}Delay`),a=r(`${bn}Duration`),c=As(i,a);let u=null,f=0,h=0;t===Lt?l>0&&(u=Lt,f=l,h=s.length):t===bn?c>0&&(u=bn,f=c,h=a.length):(f=Math.max(l,c),u=f>0?l>c?Lt:bn:null,h=u?u===Lt?s.length:a.length:0);const g=u===Lt&&/\b(transform|all)(,|$)/.test(r(`${Lt}Property`).toString());return{type:u,timeout:f,propCount:h,hasTransform:g}}function As(e,t){for(;e.lengthOs(n)+Os(e[r])))}function Os(e){return Number(e.slice(0,-1).replace(",","."))*1e3}function Su(){return document.body.offsetHeight}const Pu={esc:"escape",space:" ",up:"arrow-up",left:"arrow-left",right:"arrow-right",down:"arrow-down",delete:"backspace"},Au=(e,t)=>n=>{if(!("key"in n))return;const r=Yt(n.key);if(t.some(o=>o===r||Pu[o]===r))return e(n)},br={beforeMount(e,{value:t},{transition:n}){e._vod=e.style.display==="none"?"":e.style.display,n&&t?n.beforeEnter(e):yn(e,t)},mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)},updated(e,{value:t,oldValue:n},{transition:r}){!t!=!n&&(r?t?(r.beforeEnter(e),yn(e,!0),r.enter(e)):r.leave(e,()=>{yn(e,!1)}):yn(e,t))},beforeUnmount(e,{value:t}){yn(e,t)}};function yn(e,t){e.style.display=t?e._vod:"none"}const Ou=Ae({patchProp:wu},au);let Wr,Rs=!1;function Ru(){return Wr=Rs?Wr:Bc(Ou),Rs=!0,Wr}const Iu=(...e)=>{const t=Ru().createApp(...e),{mount:n}=t;return t.mount=r=>{const o=$u(r);if(o)return n(o,!0,o instanceof SVGElement)},t};function $u(e){return ge(e)?document.querySelector(e):e}const Nu={"v-fe360bd6":()=>W(()=>import("./develop.html-117f0576.js"),[]).then(({data:e})=>e),"v-d08d435a":()=>W(()=>import("./introduce.html-2d1ffccb.js"),[]).then(({data:e})=>e),"v-8daa1a0e":()=>W(()=>import("./index.html-c4c51566.js"),[]).then(({data:e})=>e),"v-53c50d87":()=>W(()=>import("./appium_lab.html-4a0939fc.js"),[]).then(({data:e})=>e),"v-483ce5fe":()=>W(()=>import("./extensions.html-1d1fb4fe.js"),[]).then(({data:e})=>e),"v-299549e4":()=>W(()=>import("./page_object.html-cb039342.js"),[]).then(({data:e})=>e),"v-4370387b":()=>W(()=>import("./start.html-ea9f18a1.js"),[]).then(({data:e})=>e),"v-7cbb39b9":()=>W(()=>import("./api_case.html-ba462121.js"),[]).then(({data:e})=>e),"v-109501ec":()=>W(()=>import("./api_object.html-2747dbac.js"),[]).then(({data:e})=>e),"v-d24a86f0":()=>W(()=>import("./assert.html-657c6f15.js"),[]).then(({data:e})=>e),"v-5b62ef19":()=>W(()=>import("./more.html-8b232a2d.js"),[]).then(({data:e})=>e),"v-23f9483c":()=>W(()=>import("./start.html-b31eab27.js"),[]).then(({data:e})=>e),"v-488b8cec":()=>W(()=>import("./webscocket.html-50b19ef9.js"),[]).then(({data:e})=>e),"v-d8f79a72":()=>W(()=>import("./advanced.html-75e93128.js"),[]).then(({data:e})=>e),"v-8e520eda":()=>W(()=>import("./create_project.html-3536f4e7.js"),[]).then(({data:e})=>e),"v-78c619cc":()=>W(()=>import("./data_driver.html-7c198d0a.js"),[]).then(({data:e})=>e),"v-4e6f0425":()=>W(()=>import("./dependent_func.html-f699bc84.js"),[]).then(({data:e})=>e),"v-4e8563af":()=>W(()=>import("./installation.html-02312a74.js"),[]).then(({data:e})=>e),"v-0f898c79":()=>W(()=>import("./quick_start.html-19c5f6ae.js"),[]).then(({data:e})=>e),"v-6f32df80":()=>W(()=>import("./seldom_cli.html-fcf52dd5.js"),[]).then(({data:e})=>e),"v-3cdc5c3a":()=>W(()=>import("./platform.html-c9aaf602.js"),[]).then(({data:e})=>e),"v-c350a662":()=>W(()=>import("./db_operation.html-55b16cfc.js"),[]).then(({data:e})=>e),"v-1d715ea7":()=>W(()=>import("./test_library.html-f4b42a84.js"),[]).then(({data:e})=>e),"v-471218ee":()=>W(()=>import("./browser_driver.html-3b18ca47.js"),[]).then(({data:e})=>e),"v-198befa7":()=>W(()=>import("./chaining.html-c3eac4c8.js"),[]).then(({data:e})=>e),"v-1f2c830c":()=>W(()=>import("./other.html-5da407eb.js"),[]).then(({data:e})=>e),"v-0ccdb93b":()=>W(()=>import("./page_object.html-a01824ef.js"),[]).then(({data:e})=>e),"v-40ed12b6":()=>W(()=>import("./seldom_api.html-c0b2694d.js"),[]).then(({data:e})=>e),"v-129a7066":()=>W(()=>import("./CHANGES.html-6b115101.js"),[]).then(({data:e})=>e),"v-3706649a":()=>W(()=>import("./404.html-60b35caa.js"),[]).then(({data:e})=>e)},Du=JSON.parse('{"base":"/","lang":"en-US","title":"seldom文档","description":"seldom 是基于unittest 的自动化测试框架。","head":[["link",{"rel":"icon","href":"/logo.jpeg"}]],"locales":{}}');var Mu=([e,t,n])=>e==="meta"&&t.name?`${e}.${t.name}`:["title","base"].includes(e)?e:e==="template"&&t.id?`${e}.${t.id}`:JSON.stringify([e,t,n]),Hu=e=>{const t=new Set,n=[];return e.forEach(r=>{const o=Mu(r);t.has(o)||(t.add(o),n.push(r))}),n},Wn=e=>/^(https?:)?\/\//.test(e),Fu=e=>/^mailto:/.test(e),ju=e=>/^tel:/.test(e),No=e=>Object.prototype.toString.call(e)==="[object Object]",ui=e=>e[e.length-1]==="/"?e.slice(0,-1):e,fi=e=>e[0]==="/"?e.slice(1):e,di=(e,t)=>{const n=Object.keys(e).sort((r,o)=>{const s=o.split("/").length-r.split("/").length;return s!==0?s:o.length-r.length});for(const r of n)if(t.startsWith(r))return r;return"/"};const hi={"v-fe360bd6":ve(()=>W(()=>import("./develop.html-ecb59dad.js"),[])),"v-d08d435a":ve(()=>W(()=>import("./introduce.html-27cd219e.js"),[])),"v-8daa1a0e":ve(()=>W(()=>import("./index.html-8222f902.js"),[])),"v-53c50d87":ve(()=>W(()=>import("./appium_lab.html-d907fadd.js"),[])),"v-483ce5fe":ve(()=>W(()=>import("./extensions.html-6002e3f6.js"),[])),"v-299549e4":ve(()=>W(()=>import("./page_object.html-90234acd.js"),[])),"v-4370387b":ve(()=>W(()=>import("./start.html-9ba0c0da.js"),[])),"v-7cbb39b9":ve(()=>W(()=>import("./api_case.html-8b6be0a5.js"),[])),"v-109501ec":ve(()=>W(()=>import("./api_object.html-331a6734.js"),[])),"v-d24a86f0":ve(()=>W(()=>import("./assert.html-d4c63b80.js"),[])),"v-5b62ef19":ve(()=>W(()=>import("./more.html-5e1828bb.js"),[])),"v-23f9483c":ve(()=>W(()=>import("./start.html-6c306c3b.js"),[])),"v-488b8cec":ve(()=>W(()=>import("./webscocket.html-e39e5dd8.js"),[])),"v-d8f79a72":ve(()=>W(()=>import("./advanced.html-ee1e4a3d.js"),[])),"v-8e520eda":ve(()=>W(()=>import("./create_project.html-1b343fd7.js"),[])),"v-78c619cc":ve(()=>W(()=>import("./data_driver.html-52b7c40f.js"),[])),"v-4e6f0425":ve(()=>W(()=>import("./dependent_func.html-3403aa6e.js"),[])),"v-4e8563af":ve(()=>W(()=>import("./installation.html-eed58102.js"),[])),"v-0f898c79":ve(()=>W(()=>import("./quick_start.html-564351d1.js"),[])),"v-6f32df80":ve(()=>W(()=>import("./seldom_cli.html-29118a10.js"),[])),"v-3cdc5c3a":ve(()=>W(()=>import("./platform.html-3c6229ce.js"),[])),"v-c350a662":ve(()=>W(()=>import("./db_operation.html-43b02014.js"),[])),"v-1d715ea7":ve(()=>W(()=>import("./test_library.html-dab90a67.js"),[])),"v-471218ee":ve(()=>W(()=>import("./browser_driver.html-7d1718c1.js"),[])),"v-198befa7":ve(()=>W(()=>import("./chaining.html-4d285868.js"),[])),"v-1f2c830c":ve(()=>W(()=>import("./other.html-6774d738.js"),[])),"v-0ccdb93b":ve(()=>W(()=>import("./page_object.html-bc99b214.js"),[])),"v-40ed12b6":ve(()=>W(()=>import("./seldom_api.html-e8682244.js"),[])),"v-129a7066":ve(()=>W(()=>import("./CHANGES.html-2910bbd8.js"),[])),"v-3706649a":ve(()=>W(()=>import("./404.html-b6a2d176.js"),[]))};var Bu=Symbol(""),zu=me(Nu),pi=Bn({key:"",path:"",title:"",lang:"",frontmatter:{},headers:[]}),St=me(pi),Gt=()=>St,mi=Symbol(""),gt=()=>{const e=Pe(mi);if(!e)throw new Error("usePageFrontmatter() is called without provider.");return e},gi=Symbol(""),Vu=()=>{const e=Pe(gi);if(!e)throw new Error("usePageHead() is called without provider.");return e},Uu=Symbol(""),vi=Symbol(""),qu=()=>{const e=Pe(vi);if(!e)throw new Error("usePageLang() is called without provider.");return e},_i=Symbol(""),Wu=()=>{const e=Pe(_i);if(!e)throw new Error("usePageLayout() is called without provider.");return e},Do=Symbol(""),Kn=()=>{const e=Pe(Do);if(!e)throw new Error("useRouteLocale() is called without provider.");return e},nn=me(Du),bi=()=>nn,yi=Symbol(""),Mo=()=>{const e=Pe(yi);if(!e)throw new Error("useSiteLocaleData() is called without provider.");return e},Ku=Symbol(""),Gu="Layout",Yu="NotFound",ht=vn({resolveLayouts:e=>e.reduce((t,n)=>({...t,...n.layouts}),{}),resolvePageData:async e=>{const t=zu.value[e];return await(t==null?void 0:t())??pi},resolvePageFrontmatter:e=>e.frontmatter,resolvePageHead:(e,t,n)=>{const r=ge(t.description)?t.description:n.description,o=[...Q(t.head)?t.head:[],...n.head,["title",{},e],["meta",{name:"description",content:r}]];return Hu(o)},resolvePageHeadTitle:(e,t)=>[e.title,t.title].filter(n=>!!n).join(" | "),resolvePageLang:e=>e.lang||"en",resolvePageLayout:(e,t)=>{let n;if(e.path){const r=e.frontmatter.layout;ge(r)?n=r:n=Gu}else n=Yu;return t[n]},resolveRouteLocale:(e,t)=>di(e,t),resolveSiteLocaleData:(e,t)=>({...e,...e.locales[t]})}),Ho=ue({name:"ClientOnly",setup(e,t){const n=me(!1);return Je(()=>{n.value=!0}),()=>{var r,o;return n.value?(o=(r=t.slots).default)==null?void 0:o.call(r):null}}}),Ju=ue({name:"Content",props:{pageKey:{type:String,required:!1,default:""}},setup(e){const t=Gt(),n=F(()=>hi[e.pageKey||t.value.key]);return()=>n.value?ae(n.value):ae("div","404 Not Found")}}),Dt=(e={})=>e,Fo=e=>Wn(e)?e:`/${fi(e)}`;function Ei(e,t,n){var r,o,s;t===void 0&&(t=50),n===void 0&&(n={});var l=(r=n.isImmediate)!=null&&r,i=(o=n.callback)!=null&&o,a=n.maxWait,c=Date.now(),u=[];function f(){if(a!==void 0){var g=Date.now()-c;if(g+t>=a)return a-g}return t}var h=function(){var g=[].slice.call(arguments),y=this;return new Promise(function(w,C){var v=l&&s===void 0;if(s!==void 0&&clearTimeout(s),s=setTimeout(function(){if(s=void 0,c=Date.now(),!l){var P=e.apply(y,g);i&&i(P),u.forEach(function(S){return(0,S.resolve)(P)}),u=[]}},f()),v){var b=e.apply(y,g);return i&&i(b),w(b)}u.push({resolve:w,reject:C})})};return h.cancel=function(g){s!==void 0&&clearTimeout(s),u.forEach(function(y){return(0,y.reject)(g)}),u=[]},h}/*! * vue-router v4.2.2 * (c) 2023 Eduardo San Martin Morote * @license MIT @@ -7,4 +7,4 @@ Expects a CSS selector, a Node element, a NodeList or an array. See: https://github.com/francoischalifour/medium-zoom`)}},fd=function(t){var n=document.createElement("div");return n.classList.add("medium-zoom-overlay"),n.style.background=t,n},dd=function(t){var n=t.getBoundingClientRect(),r=n.top,o=n.left,s=n.width,l=n.height,i=t.cloneNode(),a=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0,c=window.pageXOffset||document.documentElement.scrollLeft||document.body.scrollLeft||0;return i.removeAttribute("id"),i.style.position="absolute",i.style.top=r+a+"px",i.style.left=o+c+"px",i.style.width=s+"px",i.style.height=l+"px",i.style.transform="",i},en=function(t,n){var r=jt({bubbles:!1,cancelable:!1,detail:void 0},n);if(typeof window.CustomEvent=="function")return new CustomEvent(t,r);var o=document.createEvent("CustomEvent");return o.initCustomEvent(t,r.bubbles,r.cancelable,r.detail),o},hd=function e(t){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},r=window.Promise||function(L){function R(){}L(R,R)},o=function(L){var R=L.target;if(R===z){y();return}P.indexOf(R)!==-1&&w({target:R})},s=function(){if(!(U||!m.original)){var L=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0;Math.abs(Z-L)>$.scrollOffset&&setTimeout(y,150)}},l=function(L){var R=L.key||L.keyCode;(R==="Escape"||R==="Esc"||R===27)&&y()},i=function(){var L=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},R=L;if(L.background&&(z.style.background=L.background),L.container&&L.container instanceof Object&&(R.container=jt({},$.container,L.container)),L.template){var I=ar(L.template)?L.template:document.querySelector(L.template);R.template=I}return $=jt({},$,R),P.forEach(function(le){le.dispatchEvent(en("medium-zoom:update",{detail:{zoom:M}}))}),M},a=function(){var L=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};return e(jt({},$,L))},c=function(){for(var L=arguments.length,R=Array(L),I=0;I0?R.reduce(function(V,re){return[].concat(V,Qs(re))},[]):P;return le.forEach(function(V){V.classList.remove("medium-zoom-image"),V.dispatchEvent(en("medium-zoom:detach",{detail:{zoom:M}}))}),P=P.filter(function(V){return le.indexOf(V)===-1}),M},f=function(L,R){var I=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{};return P.forEach(function(le){le.addEventListener("medium-zoom:"+L,R,I)}),S.push({type:"medium-zoom:"+L,listener:R,options:I}),M},h=function(L,R){var I=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{};return P.forEach(function(le){le.removeEventListener("medium-zoom:"+L,R,I)}),S=S.filter(function(le){return!(le.type==="medium-zoom:"+L&&le.listener.toString()===R.toString())}),M},g=function(){var L=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},R=L.target,I=function(){var V={width:document.documentElement.clientWidth,height:document.documentElement.clientHeight,left:0,top:0,right:0,bottom:0},re=void 0,se=void 0;if($.container)if($.container instanceof Object)V=jt({},V,$.container),re=V.width-V.left-V.right-$.margin*2,se=V.height-V.top-V.bottom-$.margin*2;else{var He=ar($.container)?$.container:document.querySelector($.container),Ne=He.getBoundingClientRect(),Ve=Ne.width,Fe=Ne.height,Et=Ne.left,wt=Ne.top;V=jt({},V,{width:Ve,height:Fe,left:Et,top:wt})}re=re||V.width-$.margin*2,se=se||V.height-$.margin*2;var it=m.zoomedHd||m.original,$e=Js(it)?re:it.naturalWidth||re,k=Js(it)?se:it.naturalHeight||se,B=it.getBoundingClientRect(),N=B.top,K=B.left,ce=B.width,d=B.height,p=Math.min(Math.max(ce,$e),re)/ce,_=Math.min(Math.max(d,k),se)/d,E=Math.min(p,_),x=(-K+(re-ce)/2+$.margin+V.left)/E,T=(-N+(se-d)/2+$.margin+V.top)/E,H="scale("+E+") translate3d("+x+"px, "+T+"px, 0)";m.zoomed.style.transform=H,m.zoomedHd&&(m.zoomedHd.style.transform=H)};return new r(function(le){if(R&&P.indexOf(R)===-1){le(M);return}var V=function Ve(){U=!1,m.zoomed.removeEventListener("transitionend",Ve),m.original.dispatchEvent(en("medium-zoom:opened",{detail:{zoom:M}})),le(M)};if(m.zoomed){le(M);return}if(R)m.original=R;else if(P.length>0){var re=P;m.original=re[0]}else{le(M);return}if(m.original.dispatchEvent(en("medium-zoom:open",{detail:{zoom:M}})),Z=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0,U=!0,m.zoomed=dd(m.original),document.body.appendChild(z),$.template){var se=ar($.template)?$.template:document.querySelector($.template);m.template=document.createElement("div"),m.template.appendChild(se.content.cloneNode(!0)),document.body.appendChild(m.template)}if(m.original.parentElement&&m.original.parentElement.tagName==="PICTURE"&&m.original.currentSrc&&(m.zoomed.src=m.original.currentSrc),document.body.appendChild(m.zoomed),window.requestAnimationFrame(function(){document.body.classList.add("medium-zoom--opened")}),m.original.classList.add("medium-zoom-image--hidden"),m.zoomed.classList.add("medium-zoom-image--opened"),m.zoomed.addEventListener("click",y),m.zoomed.addEventListener("transitionend",V),m.original.getAttribute("data-zoom-src")){m.zoomedHd=m.zoomed.cloneNode(),m.zoomedHd.removeAttribute("srcset"),m.zoomedHd.removeAttribute("sizes"),m.zoomedHd.removeAttribute("loading"),m.zoomedHd.src=m.zoomed.getAttribute("data-zoom-src"),m.zoomedHd.onerror=function(){clearInterval(He),console.warn("Unable to reach the zoom image target "+m.zoomedHd.src),m.zoomedHd=null,I()};var He=setInterval(function(){m.zoomedHd.complete&&(clearInterval(He),m.zoomedHd.classList.add("medium-zoom-image--opened"),m.zoomedHd.addEventListener("click",y),document.body.appendChild(m.zoomedHd),I())},10)}else if(m.original.hasAttribute("srcset")){m.zoomedHd=m.zoomed.cloneNode(),m.zoomedHd.removeAttribute("sizes"),m.zoomedHd.removeAttribute("loading");var Ne=m.zoomedHd.addEventListener("load",function(){m.zoomedHd.removeEventListener("load",Ne),m.zoomedHd.classList.add("medium-zoom-image--opened"),m.zoomedHd.addEventListener("click",y),document.body.appendChild(m.zoomedHd),I()})}else I()})},y=function(){return new r(function(L){if(U||!m.original){L(M);return}var R=function I(){m.original.classList.remove("medium-zoom-image--hidden"),document.body.removeChild(m.zoomed),m.zoomedHd&&document.body.removeChild(m.zoomedHd),document.body.removeChild(z),m.zoomed.classList.remove("medium-zoom-image--opened"),m.template&&document.body.removeChild(m.template),U=!1,m.zoomed.removeEventListener("transitionend",I),m.original.dispatchEvent(en("medium-zoom:closed",{detail:{zoom:M}})),m.original=null,m.zoomed=null,m.zoomedHd=null,m.template=null,L(M)};U=!0,document.body.classList.remove("medium-zoom--opened"),m.zoomed.style.transform="",m.zoomedHd&&(m.zoomedHd.style.transform=""),m.template&&(m.template.style.transition="opacity 150ms",m.template.style.opacity=0),m.original.dispatchEvent(en("medium-zoom:close",{detail:{zoom:M}})),m.zoomed.addEventListener("transitionend",R)})},w=function(){var L=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},R=L.target;return m.original?y():g({target:R})},C=function(){return $},v=function(){return P},b=function(){return m.original},P=[],S=[],U=!1,Z=0,$=n,m={original:null,zoomed:null,zoomedHd:null,template:null};Object.prototype.toString.call(t)==="[object Object]"?$=t:(t||typeof t=="string")&&c(t),$=jt({margin:0,background:"#fff",scrollOffset:40,container:null,template:null},$);var z=fd($.background);document.addEventListener("click",o),document.addEventListener("keyup",l),document.addEventListener("scroll",s),window.addEventListener("resize",y);var M={open:g,close:y,toggle:w,update:i,clone:a,attach:c,detach:u,on:f,off:h,getOptions:C,getImages:v,getZoomedImage:b};return M};function pd(e,t){t===void 0&&(t={});var n=t.insertAt;if(!(!e||typeof document>"u")){var r=document.head||document.getElementsByTagName("head")[0],o=document.createElement("style");o.type="text/css",n==="top"&&r.firstChild?r.insertBefore(o,r.firstChild):r.appendChild(o),o.styleSheet?o.styleSheet.cssText=e:o.appendChild(document.createTextNode(e))}}var md=".medium-zoom-overlay{position:fixed;top:0;right:0;bottom:0;left:0;opacity:0;transition:opacity .3s;will-change:opacity}.medium-zoom--opened .medium-zoom-overlay{cursor:pointer;cursor:zoom-out;opacity:1}.medium-zoom-image{cursor:pointer;cursor:zoom-in;transition:transform .3s cubic-bezier(.2,0,.2,1)!important}.medium-zoom-image--hidden{visibility:hidden}.medium-zoom-image--opened{position:relative;cursor:pointer;cursor:zoom-out;will-change:transform}";pd(md);const gd=hd,vd=Symbol("mediumZoom");const _d=".theme-default-content > img, .theme-default-content :not(a) > img",bd={},yd=300,Ed=Dt({enhance({app:e,router:t}){const n=gd(bd);n.refresh=(r=_d)=>{n.detach(),n.attach(r)},e.provide(vd,n),t.afterEach(()=>{setTimeout(()=>n.refresh(),yd)})}});/** * NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress * @license MIT - */const fe={settings:{minimum:.08,easing:"ease",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,barSelector:'[role="bar"]',parent:"body",template:'
    '},status:null,set:e=>{const t=fe.isStarted();e=Jr(e,fe.settings.minimum,1),fe.status=e===1?null:e;const n=fe.render(!t),r=n.querySelector(fe.settings.barSelector),o=fe.settings.speed,s=fe.settings.easing;return n.offsetWidth,wd(l=>{rr(r,{transform:"translate3d("+Zs(e)+"%,0,0)",transition:"all "+o+"ms "+s}),e===1?(rr(n,{transition:"none",opacity:"1"}),n.offsetWidth,setTimeout(function(){rr(n,{transition:"all "+o+"ms linear",opacity:"0"}),setTimeout(function(){fe.remove(),l()},o)},o)):setTimeout(()=>l(),o)}),fe},isStarted:()=>typeof fe.status=="number",start:()=>{fe.status||fe.set(0);const e=()=>{setTimeout(()=>{fe.status&&(fe.trickle(),e())},fe.settings.trickleSpeed)};return fe.settings.trickle&&e(),fe},done:e=>!e&&!fe.status?fe:fe.inc(.3+.5*Math.random()).set(1),inc:e=>{let t=fe.status;return t?(typeof e!="number"&&(e=(1-t)*Jr(Math.random()*t,.1,.95)),t=Jr(t+e,0,.994),fe.set(t)):fe.start()},trickle:()=>fe.inc(Math.random()*fe.settings.trickleRate),render:e=>{if(fe.isRendered())return document.getElementById("nprogress");Xs(document.documentElement,"nprogress-busy");const t=document.createElement("div");t.id="nprogress",t.innerHTML=fe.settings.template;const n=t.querySelector(fe.settings.barSelector),r=e?"-100":Zs(fe.status||0),o=document.querySelector(fe.settings.parent);return rr(n,{transition:"all 0 linear",transform:"translate3d("+r+"%,0,0)"}),o!==document.body&&Xs(o,"nprogress-custom-parent"),o==null||o.appendChild(t),t},remove:()=>{el(document.documentElement,"nprogress-busy"),el(document.querySelector(fe.settings.parent),"nprogress-custom-parent");const e=document.getElementById("nprogress");e&&kd(e)},isRendered:()=>!!document.getElementById("nprogress")},Jr=(e,t,n)=>en?n:e,Zs=e=>(-1+e)*100,wd=function(){const e=[];function t(){const n=e.shift();n&&n(t)}return function(n){e.push(n),e.length===1&&t()}}(),rr=function(){const e=["Webkit","O","Moz","ms"],t={};function n(l){return l.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,function(i,a){return a.toUpperCase()})}function r(l){const i=document.body.style;if(l in i)return l;let a=e.length;const c=l.charAt(0).toUpperCase()+l.slice(1);let u;for(;a--;)if(u=e[a]+c,u in i)return u;return l}function o(l){return l=n(l),t[l]??(t[l]=r(l))}function s(l,i,a){i=o(i),l.style[i]=a}return function(l,i){for(const a in i){const c=i[a];c!==void 0&&Object.prototype.hasOwnProperty.call(i,a)&&s(l,a,c)}}}(),Ii=(e,t)=>(typeof e=="string"?e:zo(e)).indexOf(" "+t+" ")>=0,Xs=(e,t)=>{const n=zo(e),r=n+t;Ii(n,t)||(e.className=r.substring(1))},el=(e,t)=>{const n=zo(e);if(!Ii(e,t))return;const r=n.replace(" "+t+" "," ");e.className=r.substring(1,r.length-1)},zo=e=>(" "+(e.className||"")+" ").replace(/\s+/gi," "),kd=e=>{e&&e.parentNode&&e.parentNode.removeChild(e)};const xd=()=>{Je(()=>{const e=Jt(),t=new Set;t.add(e.currentRoute.value.path),e.beforeEach(n=>{t.has(n.path)||fe.start()}),e.afterEach(n=>{t.add(n.path),fe.done()})})},Ld=Dt({setup(){xd()}}),Cd=JSON.parse(`{"repo":"SeldomQA/seldom","docsBranch":"vuepress-docs/docs/vpdocs","logo":"/logo.jpeg","navbar":[{"text":"介绍","link":"/introduce"},{"text":"安装","link":"/getting-started/installation"}],"sidebar":["/introduce",{"text":"开始","children":["/getting-started/installation","/getting-started/create_project","/getting-started/quick_start","/getting-started/advanced","/getting-started/data_driver","/getting-started/dependent_func","/getting-started/seldom_cli"]},{"text":"web UI 测试","children":["/web-testing/browser_driver","/web-testing/seldom_api","/web-testing/chaining","/web-testing/page_object","/web-testing/other"]},{"text":"App UI 测试","children":["/app-testing/start","/app-testing/appium_lab","/app-testing/page_object","/app-testing/extensions"]},{"text":"HTTP接口测试","children":["/api-testing/start","/api-testing/assert","/api-testing/api_object","/api-testing/more","/api-testing/api_case","/api-testing/webscocket"]},{"text":"更多能力","children":["/more-ability/db_operation","/more-ability/test_library"]},"/platform/platform","/version/CHANGES"],"editLinks":true,"editLinkText":"在 GitHub 上编辑此页","lastUpdated":"上次更新","locales":{"/":{"selectLanguageName":"English"}},"colorMode":"auto","colorModeSwitch":true,"selectLanguageText":"Languages","selectLanguageAriaLabel":"Select language","sidebarDepth":2,"editLink":true,"lastUpdatedText":"Last Updated","contributors":true,"contributorsText":"Contributors","notFound":["There's nothing here.","How did we get here?","That's a Four-Oh-Four.","Looks like we've got some broken links."],"backToHome":"Take me home","openInNewWindow":"open in new window","toggleColorMode":"toggle color mode","toggleSidebar":"toggle sidebar"}`),Td=me(Cd),$i=()=>Td,Ni=Symbol(""),Sd=()=>{const e=Pe(Ni);if(!e)throw new Error("useThemeLocaleData() is called without provider.");return e},Pd=(e,t)=>{const{locales:n,...r}=e;return{...r,...n==null?void 0:n[t]}},Ad=Dt({enhance({app:e}){const t=$i(),n=e._context.provides[Do],r=F(()=>Pd(t.value,n.value));e.provide(Ni,r),Object.defineProperties(e.config.globalProperties,{$theme:{get(){return t.value}},$themeLocale:{get(){return r.value}}})}}),Od=ue({__name:"Badge",props:{type:{type:String,required:!1,default:"tip"},text:{type:String,required:!1,default:""},vertical:{type:String,required:!1,default:void 0}},setup(e){return(t,n)=>(j(),X("span",{class:qe(["badge",e.type]),style:jn({verticalAlign:e.vertical})},[Ee(t.$slots,"default",{},()=>[Nt(Re(e.text),1)])],6))}}),Le=(e,t)=>{const n=e.__vccOpts||e;for(const[r,o]of t)n[r]=o;return n},Rd=Le(Od,[["__file","Badge.vue"]]),Id=ue({name:"CodeGroup",slots:Object,setup(e,{slots:t}){const n=me(-1),r=me([]),o=(i=n.value)=>{i{i>0?n.value=i-1:n.value=r.value.length-1,r.value[n.value].focus()},l=(i,a)=>{i.key===" "||i.key==="Enter"?(i.preventDefault(),n.value=a):i.key==="ArrowRight"?(i.preventDefault(),o(a)):i.key==="ArrowLeft"&&(i.preventDefault(),s(a))};return()=>{var a;const i=(((a=t.default)==null?void 0:a.call(t))||[]).filter(c=>c.type.name==="CodeGroupItem").map(c=>(c.props===null&&(c.props={}),c));return i.length===0?null:(n.value<0||n.value>i.length-1?(n.value=i.findIndex(c=>c.props.active===""||c.props.active===!0),n.value===-1&&(n.value=0)):i.forEach((c,u)=>{c.props.active=u===n.value}),ae("div",{class:"code-group"},[ae("div",{class:"code-group__nav"},ae("ul",{class:"code-group__ul"},i.map((c,u)=>{const f=u===n.value;return ae("li",{class:"code-group__li"},ae("button",{ref:h=>{h&&(r.value[u]=h)},class:{"code-group__nav-tab":!0,"code-group__nav-tab-active":f},ariaPressed:f,ariaExpanded:f,onClick:()=>n.value=u,onKeydown:h=>l(h,u)},c.props.title))}))),i]))}}}),$d=["aria-selected"],Nd=ue({name:"CodeGroupItem"}),Dd=ue({...Nd,props:{title:{type:String,required:!0},active:{type:Boolean,required:!1,default:!1}},setup(e){return(t,n)=>(j(),X("div",{class:qe(["code-group-item",{"code-group-item__active":e.active}]),"aria-selected":e.active},[Ee(t.$slots,"default")],10,$d))}}),Md=Le(Dd,[["__file","CodeGroupItem.vue"]]);function Di(e){return yl()?(ma(e),!0):!1}function Mn(e){return typeof e=="function"?e():ee(e)}const Hd=typeof window<"u",Mi=()=>{};function Fd(e,t){function n(...r){return new Promise((o,s)=>{Promise.resolve(e(()=>t.apply(this,r),{fn:t,thisArg:this,args:r})).then(o).catch(s)})}return n}const Hi=e=>e();function jd(e=Hi){const t=me(!0);function n(){t.value=!1}function r(){t.value=!0}const o=(...s)=>{t.value&&e(...s)};return{isActive:Bn(t),pause:n,resume:r,eventFilter:o}}function Bd(...e){if(e.length!==1)return Ja(...e);const t=e[0];return typeof t=="function"?Bn(Ka(()=>({get:t,set:Mi}))):me(t)}function zd(e=!1,t={}){const{truthyValue:n=!0,falsyValue:r=!1}=t,o=Ie(e),s=me(e);function l(i){if(arguments.length)return s.value=i,s.value;{const a=Mn(n);return s.value=s.value===a?Mn(r):a,s.value}}return o?l:[s,l]}var tl=Object.getOwnPropertySymbols,Vd=Object.prototype.hasOwnProperty,Ud=Object.prototype.propertyIsEnumerable,qd=(e,t)=>{var n={};for(var r in e)Vd.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(e!=null&&tl)for(var r of tl(e))t.indexOf(r)<0&&Ud.call(e,r)&&(n[r]=e[r]);return n};function Wd(e,t,n={}){const r=n,{eventFilter:o=Hi}=r,s=qd(r,["eventFilter"]);return st(e,Fd(o,t),s)}var Kd=Object.defineProperty,Gd=Object.defineProperties,Yd=Object.getOwnPropertyDescriptors,Er=Object.getOwnPropertySymbols,Fi=Object.prototype.hasOwnProperty,ji=Object.prototype.propertyIsEnumerable,nl=(e,t,n)=>t in e?Kd(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,Jd=(e,t)=>{for(var n in t||(t={}))Fi.call(t,n)&&nl(e,n,t[n]);if(Er)for(var n of Er(t))ji.call(t,n)&&nl(e,n,t[n]);return e},Qd=(e,t)=>Gd(e,Yd(t)),Zd=(e,t)=>{var n={};for(var r in e)Fi.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(e!=null&&Er)for(var r of Er(e))t.indexOf(r)<0&&ji.call(e,r)&&(n[r]=e[r]);return n};function Xd(e,t,n={}){const r=n,{eventFilter:o}=r,s=Zd(r,["eventFilter"]),{eventFilter:l,pause:i,resume:a,isActive:c}=jd(o);return{stop:Wd(e,t,Qd(Jd({},s),{eventFilter:l})),pause:i,resume:a,isActive:c}}function eh(e){var t;const n=Mn(e);return(t=n==null?void 0:n.$el)!=null?t:n}const wr=Hd?window:void 0;function rl(...e){let t,n,r,o;if(typeof e[0]=="string"||Array.isArray(e[0])?([n,r,o]=e,t=wr):[t,n,r,o]=e,!t)return Mi;Array.isArray(n)||(n=[n]),Array.isArray(r)||(r=[r]);const s=[],l=()=>{s.forEach(u=>u()),s.length=0},i=(u,f,h,g)=>(u.addEventListener(f,h,g),()=>u.removeEventListener(f,h,g)),a=st(()=>[eh(t),Mn(o)],([u,f])=>{l(),u&&s.push(...n.flatMap(h=>r.map(g=>i(u,h,g,f))))},{immediate:!0,flush:"post"}),c=()=>{a(),l()};return Di(c),c}function th(){const e=me(!1);return li()&&Je(()=>{e.value=!0}),e}function nh(e){const t=th();return F(()=>(t.value,!!e()))}function rh(e,t={}){const{window:n=wr}=t,r=nh(()=>n&&"matchMedia"in n&&typeof n.matchMedia=="function");let o;const s=me(!1),l=()=>{o&&("removeEventListener"in o?o.removeEventListener("change",i):o.removeListener(i))},i=()=>{r.value&&(l(),o=n.matchMedia(Bd(e).value),s.value=!!(o!=null&&o.matches),o&&("addEventListener"in o?o.addEventListener("change",i):o.addListener(i)))};return uc(i),Di(()=>l()),s}const or=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},sr="__vueuse_ssr_handlers__",oh=sh();function sh(){return sr in or||(or[sr]=or[sr]||{}),or[sr]}function lh(e,t){return oh[e]||t}function ih(e){return e==null?"any":e instanceof Set?"set":e instanceof Map?"map":e instanceof Date?"date":typeof e=="boolean"?"boolean":typeof e=="string"?"string":typeof e=="object"?"object":Number.isNaN(e)?"any":"number"}var ah=Object.defineProperty,ol=Object.getOwnPropertySymbols,ch=Object.prototype.hasOwnProperty,uh=Object.prototype.propertyIsEnumerable,sl=(e,t,n)=>t in e?ah(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,ll=(e,t)=>{for(var n in t||(t={}))ch.call(t,n)&&sl(e,n,t[n]);if(ol)for(var n of ol(t))uh.call(t,n)&&sl(e,n,t[n]);return e};const fh={boolean:{read:e=>e==="true",write:e=>String(e)},object:{read:e=>JSON.parse(e),write:e=>JSON.stringify(e)},number:{read:e=>Number.parseFloat(e),write:e=>String(e)},any:{read:e=>e,write:e=>String(e)},string:{read:e=>e,write:e=>String(e)},map:{read:e=>new Map(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e.entries()))},set:{read:e=>new Set(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e))},date:{read:e=>new Date(e),write:e=>e.toISOString()}},il="vueuse-storage";function dh(e,t,n,r={}){var o;const{flush:s="pre",deep:l=!0,listenToStorageChanges:i=!0,writeDefaults:a=!0,mergeDefaults:c=!1,shallow:u,window:f=wr,eventFilter:h,onError:g=m=>{console.error(m)}}=r,y=(u?Il:me)(t);if(!n)try{n=lh("getDefaultStorage",()=>{var m;return(m=wr)==null?void 0:m.localStorage})()}catch(m){g(m)}if(!n)return y;const w=Mn(t),C=ih(w),v=(o=r.serializer)!=null?o:fh[C],{pause:b,resume:P}=Xd(y,()=>S(y.value),{flush:s,deep:l,eventFilter:h});return f&&i&&(rl(f,"storage",$),rl(f,il,Z)),$(),y;function S(m){try{if(m==null)n.removeItem(e);else{const z=v.write(m),M=n.getItem(e);M!==z&&(n.setItem(e,z),f&&f.dispatchEvent(new CustomEvent(il,{detail:{key:e,oldValue:M,newValue:z,storageArea:n}})))}}catch(z){g(z)}}function U(m){const z=m?m.newValue:n.getItem(e);if(z==null)return a&&w!==null&&n.setItem(e,v.write(w)),w;if(!m&&c){const M=v.read(z);return typeof c=="function"?c(M,w):C==="object"&&!Array.isArray(M)?ll(ll({},w),M):M}else return typeof z!="string"?z:v.read(z)}function Z(m){$(m.detail)}function $(m){if(!(m&&m.storageArea!==n)){if(m&&m.key==null){y.value=w;return}if(!(m&&m.key!==e)){b();try{y.value=U(m)}catch(z){g(z)}finally{m?Sr(P):P()}}}}}function hh(e){return rh("(prefers-color-scheme: dark)",e)}const ph=()=>$i(),ze=()=>Sd(),Bi=Symbol(""),Vo=()=>{const e=Pe(Bi);if(!e)throw new Error("useDarkMode() is called without provider.");return e},mh=()=>{const e=ze(),t=hh(),n=dh("vuepress-color-scheme",e.value.colorMode),r=F({get(){return e.value.colorModeSwitch?n.value==="auto"?t.value:n.value==="dark":e.value.colorMode==="dark"},set(o){o===t.value?n.value="auto":n.value=o?"dark":"light"}});Wt(Bi,r),gh(r)},gh=e=>{const t=(n=e.value)=>{const r=window==null?void 0:window.document.querySelector("html");r==null||r.classList.toggle("dark",n)};Je(()=>{st(e,t,{immediate:!0})}),Rr(()=>t())},zi=(...e)=>{const n=Jt().resolve(...e),r=n.matched[n.matched.length-1];if(!(r!=null&&r.redirect))return n;const{redirect:o}=r,s=oe(o)?o(n):o,l=ge(s)?{path:s}:s;return zi({hash:n.hash,query:n.query,params:n.params,...l})},Uo=e=>{const t=zi(encodeURI(e));return{text:t.meta.title||e,link:t.name==="404"?e:t.fullPath}};let Qr=null,wn=null;const vh={wait:()=>Qr,pending:()=>{Qr=new Promise(e=>wn=e)},resolve:()=>{wn==null||wn(),Qr=null,wn=null}},Vi=()=>vh,Ui=Symbol("sidebarItems"),qo=()=>{const e=Pe(Ui);if(!e)throw new Error("useSidebarItems() is called without provider.");return e},_h=()=>{const e=ze(),t=gt(),n=F(()=>bh(t.value,e.value));Wt(Ui,n)},bh=(e,t)=>{const n=e.sidebar??t.sidebar??"auto",r=e.sidebarDepth??t.sidebarDepth??2;return e.home||n===!1?[]:n==="auto"?Eh(r):Q(n)?qi(n,r):No(n)?wh(n,r):[]},yh=(e,t)=>({text:e.title,link:e.link,children:Wo(e.children,t)}),Wo=(e,t)=>t>0?e.map(n=>yh(n,t-1)):[],Eh=e=>{const t=Gt();return[{text:t.value.title,children:Wo(t.value.headers,e)}]},qi=(e,t)=>{const n=Qt(),r=Gt(),o=s=>{var i;let l;if(ge(s)?l=Uo(s):l=s,l.children)return{...l,children:l.children.map(a=>o(a))};if(l.link===n.path){const a=((i=r.value.headers[0])==null?void 0:i.level)===1?r.value.headers[0].children:r.value.headers;return{...l,children:Wo(a,t)}}return l};return e.map(s=>o(s))},wh=(e,t)=>{const n=Qt(),r=di(e,n.path),o=e[r]??[];return qi(o,t)},kh="719px",xh={mobile:kh};var Hn;(function(e){e.MOBILE="mobile"})(Hn||(Hn={}));var hl;const Lh={[Hn.MOBILE]:Number.parseInt((hl=xh.mobile)==null?void 0:hl.replace("px",""),10)},Wi=(e,t)=>{const n=Lh[e];Number.isInteger(n)&&Je(()=>{t(n),window.addEventListener("resize",()=>t(n),!1),window.addEventListener("orientationchange",()=>t(n),!1)})},Ch={},Th={class:"theme-default-content"};function Sh(e,t){const n=bt("Content");return j(),X("div",Th,[te(n)])}const Ph=Le(Ch,[["render",Sh],["__file","HomeContent.vue"]]),Ah={key:0,class:"features"},Oh=ue({__name:"HomeFeatures",setup(e){const t=gt(),n=F(()=>Q(t.value.features)?t.value.features:[]);return(r,o)=>n.value.length?(j(),X("div",Ah,[(j(!0),X(we,null,It(n.value,s=>(j(),X("div",{key:s.title,class:"feature"},[he("h2",null,Re(s.title),1),he("p",null,Re(s.details),1)]))),128))])):Ce("v-if",!0)}}),Rh=Le(Oh,[["__file","HomeFeatures.vue"]]),Ih=["innerHTML"],$h=["textContent"],Nh=ue({__name:"HomeFooter",setup(e){const t=gt(),n=F(()=>t.value.footer),r=F(()=>t.value.footerHtml);return(o,s)=>n.value?(j(),X(we,{key:0},[Ce(" eslint-disable-next-line vue/no-v-html "),r.value?(j(),X("div",{key:0,class:"footer",innerHTML:n.value},null,8,Ih)):(j(),X("div",{key:1,class:"footer",textContent:Re(n.value)},null,8,$h))],64)):Ce("v-if",!0)}}),Dh=Le(Nh,[["__file","HomeFooter.vue"]]),Mh=["href","rel","target","aria-label"],Hh=ue({inheritAttrs:!1}),Fh=ue({...Hh,__name:"AutoLink",props:{item:{type:Object,required:!0}},setup(e){const t=e,n=Qt(),r=bi(),{item:o}=Tr(t),s=F(()=>Wn(o.value.link)),l=F(()=>Fu(o.value.link)||ju(o.value.link)),i=F(()=>{if(!l.value){if(o.value.target)return o.value.target;if(s.value)return"_blank"}}),a=F(()=>i.value==="_blank"),c=F(()=>!s.value&&!l.value&&!a.value),u=F(()=>{if(!l.value){if(o.value.rel)return o.value.rel;if(a.value)return"noopener noreferrer"}}),f=F(()=>o.value.ariaLabel||o.value.text),h=F(()=>{const w=Object.keys(r.value.locales);return w.length?!w.some(C=>C===o.value.link):o.value.link!=="/"}),g=F(()=>h.value?n.path.startsWith(o.value.link):!1),y=F(()=>c.value?o.value.activeMatch?new RegExp(o.value.activeMatch).test(n.path):g.value:!1);return(w,C)=>{const v=bt("RouterLink"),b=bt("AutoLinkExternalIcon");return c.value?(j(),Se(v,co({key:0,class:{"router-link-active":y.value},to:ee(o).link,"aria-label":f.value},w.$attrs),{default:De(()=>[Ee(w.$slots,"before"),Nt(" "+Re(ee(o).text)+" ",1),Ee(w.$slots,"after")]),_:3},16,["class","to","aria-label"])):(j(),X("a",co({key:1,class:"external-link",href:ee(o).link,rel:u.value,target:i.value,"aria-label":f.value},w.$attrs),[Ee(w.$slots,"before"),Nt(" "+Re(ee(o).text)+" ",1),a.value?(j(),Se(b,{key:0})):Ce("v-if",!0),Ee(w.$slots,"after")],16,Mh))}}}),vt=Le(Fh,[["__file","AutoLink.vue"]]),jh={class:"hero"},Bh={key:0,id:"main-title"},zh={key:1,class:"description"},Vh={key:2,class:"actions"},Uh=ue({__name:"HomeHero",setup(e){const t=gt(),n=Mo(),r=Vo(),o=F(()=>r.value&&t.value.heroImageDark!==void 0?t.value.heroImageDark:t.value.heroImage),s=F(()=>t.value.heroAlt||i.value||"hero"),l=F(()=>t.value.heroHeight||280),i=F(()=>t.value.heroText===null?null:t.value.heroText||n.value.title||"Hello"),a=F(()=>t.value.tagline===null?null:t.value.tagline||n.value.description||"Welcome to your VuePress site"),c=F(()=>Q(t.value.actions)?t.value.actions.map(({text:f,link:h,type:g="primary"})=>({text:f,link:h,type:g})):[]),u=()=>{if(!o.value)return null;const f=ae("img",{src:Fo(o.value),alt:s.value,height:l.value});return t.value.heroImageDark===void 0?f:ae(Ho,()=>f)};return(f,h)=>(j(),X("header",jh,[te(u),i.value?(j(),X("h1",Bh,Re(i.value),1)):Ce("v-if",!0),a.value?(j(),X("p",zh,Re(a.value),1)):Ce("v-if",!0),c.value.length?(j(),X("p",Vh,[(j(!0),X(we,null,It(c.value,g=>(j(),Se(vt,{key:g.text,class:qe(["action-button",[g.type]]),item:g},null,8,["class","item"]))),128))])):Ce("v-if",!0)]))}}),qh=Le(Uh,[["__file","HomeHero.vue"]]),Wh={class:"home"},Kh=ue({__name:"Home",setup(e){return(t,n)=>(j(),X("main",Wh,[te(qh),te(Rh),te(Ph),te(Dh)]))}}),Gh=Le(Kh,[["__file","Home.vue"]]),Yh=ue({__name:"NavbarBrand",setup(e){const t=Kn(),n=Mo(),r=ze(),o=Vo(),s=F(()=>r.value.home||t.value),l=F(()=>n.value.title),i=F(()=>o.value&&r.value.logoDark!==void 0?r.value.logoDark:r.value.logo),a=()=>{if(!i.value)return null;const c=ae("img",{class:"logo",src:Fo(i.value),alt:l.value});return r.value.logoDark===void 0?c:ae(Ho,()=>c)};return(c,u)=>{const f=bt("RouterLink");return j(),Se(f,{to:s.value},{default:De(()=>[te(a),l.value?(j(),X("span",{key:0,class:qe(["site-name",{"can-hide":i.value}])},Re(l.value),3)):Ce("v-if",!0)]),_:1},8,["to"])}}}),Jh=Le(Yh,[["__file","NavbarBrand.vue"]]),Qh=ue({__name:"DropdownTransition",setup(e){const t=r=>{r.style.height=r.scrollHeight+"px"},n=r=>{r.style.height=""};return(r,o)=>(j(),Se(qn,{name:"dropdown",onEnter:t,onAfterEnter:n,onBeforeLeave:t},{default:De(()=>[Ee(r.$slots,"default")]),_:3}))}}),Ki=Le(Qh,[["__file","DropdownTransition.vue"]]),Zh=["aria-label"],Xh={class:"title"},ep=he("span",{class:"arrow down"},null,-1),tp=["aria-label"],np={class:"title"},rp={class:"navbar-dropdown"},op={class:"navbar-dropdown-subtitle"},sp={key:1},lp={class:"navbar-dropdown-subitem-wrapper"},ip=ue({__name:"NavbarDropdown",props:{item:{type:Object,required:!0}},setup(e){const t=e,{item:n}=Tr(t),r=F(()=>n.value.ariaLabel||n.value.text),o=me(!1),s=Qt();st(()=>s.path,()=>{o.value=!1});const l=a=>{a.detail===0?o.value=!o.value:o.value=!1},i=(a,c)=>c[c.length-1]===a;return(a,c)=>(j(),X("div",{class:qe(["navbar-dropdown-wrapper",{open:o.value}])},[he("button",{class:"navbar-dropdown-title",type:"button","aria-label":r.value,onClick:l},[he("span",Xh,Re(ee(n).text),1),ep],8,Zh),he("button",{class:"navbar-dropdown-title-mobile",type:"button","aria-label":r.value,onClick:c[0]||(c[0]=u=>o.value=!o.value)},[he("span",np,Re(ee(n).text),1),he("span",{class:qe(["arrow",o.value?"down":"right"])},null,2)],8,tp),te(Ki,null,{default:De(()=>[pr(he("ul",rp,[(j(!0),X(we,null,It(ee(n).children,u=>(j(),X("li",{key:u.text,class:"navbar-dropdown-item"},[u.children?(j(),X(we,{key:0},[he("h4",op,[u.link?(j(),Se(vt,{key:0,item:u,onFocusout:f=>i(u,ee(n).children)&&u.children.length===0&&(o.value=!1)},null,8,["item","onFocusout"])):(j(),X("span",sp,Re(u.text),1))]),he("ul",lp,[(j(!0),X(we,null,It(u.children,f=>(j(),X("li",{key:f.link,class:"navbar-dropdown-subitem"},[te(vt,{item:f,onFocusout:h=>i(f,u.children)&&i(u,ee(n).children)&&(o.value=!1)},null,8,["item","onFocusout"])]))),128))])],64)):(j(),Se(vt,{key:1,item:u,onFocusout:f=>i(u,ee(n).children)&&(o.value=!1)},null,8,["item","onFocusout"]))]))),128))],512),[[br,o.value]])]),_:1})],2))}}),ap=Le(ip,[["__file","NavbarDropdown.vue"]]),al=e=>decodeURI(e).replace(/#.*$/,"").replace(/(index)?\.(md|html)$/,""),cp=(e,t)=>{if(t.hash===e)return!0;const n=al(t.path),r=al(e);return n===r},Gi=(e,t)=>e.link&&cp(e.link,t)?!0:e.children?e.children.some(n=>Gi(n,t)):!1,Yi=e=>!Wn(e)||/github\.com/.test(e)?"GitHub":/bitbucket\.org/.test(e)?"Bitbucket":/gitlab\.com/.test(e)?"GitLab":/gitee\.com/.test(e)?"Gitee":null,up={GitHub:":repo/edit/:branch/:path",GitLab:":repo/-/edit/:branch/:path",Gitee:":repo/edit/:branch/:path",Bitbucket:":repo/src/:branch/:path?mode=edit&spa=0&at=:branch&fileviewer=file-view-default"},fp=({docsRepo:e,editLinkPattern:t})=>{if(t)return t;const n=Yi(e);return n!==null?up[n]:null},dp=({docsRepo:e,docsBranch:t,docsDir:n,filePathRelative:r,editLinkPattern:o})=>{if(!r)return null;const s=fp({docsRepo:e,editLinkPattern:o});return s?s.replace(/:repo/,Wn(e)?e:`https://github.com/${e}`).replace(/:branch/,t).replace(/:path/,fi(`${ui(n)}/${r}`)):null},hp={key:0,class:"navbar-items"},pp=ue({__name:"NavbarItems",setup(e){const t=()=>{const u=Jt(),f=Kn(),h=bi(),g=Mo(),y=ph(),w=ze();return F(()=>{const C=Object.keys(h.value.locales);if(C.length<2)return[];const v=u.currentRoute.value.path,b=u.currentRoute.value.fullPath;return[{text:`${w.value.selectLanguageText}`,ariaLabel:`${w.value.selectLanguageAriaLabel??w.value.selectLanguageText}`,children:C.map(S=>{var M,G;const U=((M=h.value.locales)==null?void 0:M[S])??{},Z=((G=y.value.locales)==null?void 0:G[S])??{},$=`${U.lang}`,m=Z.selectLanguageName??$;let z;if($===g.value.lang)z=b;else{const L=v.replace(f.value,S);u.getRoutes().some(R=>R.path===L)?z=b.replace(v,L):z=Z.home??S}return{text:m,link:z}})}]})},n=()=>{const u=ze(),f=F(()=>u.value.repo),h=F(()=>f.value?Yi(f.value):null),g=F(()=>f.value&&!Wn(f.value)?`https://github.com/${f.value}`:f.value),y=F(()=>g.value?u.value.repoLabel?u.value.repoLabel:h.value===null?"Source":h.value:null);return F(()=>!g.value||!y.value?[]:[{text:y.value,link:g.value}])},r=u=>ge(u)?Uo(u):u.children?{...u,children:u.children.map(r)}:u,o=()=>{const u=ze();return F(()=>(u.value.navbar||[]).map(r))},s=me(!1),l=o(),i=t(),a=n(),c=F(()=>[...l.value,...i.value,...a.value]);return Wi(Hn.MOBILE,u=>{window.innerWidthc.value.length?(j(),X("nav",hp,[(j(!0),X(we,null,It(c.value,h=>(j(),X("div",{key:h.text,class:"navbar-item"},[h.children?(j(),Se(ap,{key:0,item:h,class:qe(s.value?"mobile":"")},null,8,["item","class"])):(j(),Se(vt,{key:1,item:h},null,8,["item"]))]))),128))])):Ce("v-if",!0)}}),Ji=Le(pp,[["__file","NavbarItems.vue"]]),mp=["title"],gp={class:"icon",focusable:"false",viewBox:"0 0 32 32"},vp=Gc('',9),_p=[vp],bp={class:"icon",focusable:"false",viewBox:"0 0 32 32"},yp=he("path",{d:"M13.502 5.414a15.075 15.075 0 0 0 11.594 18.194a11.113 11.113 0 0 1-7.975 3.39c-.138 0-.278.005-.418 0a11.094 11.094 0 0 1-3.2-21.584M14.98 3a1.002 1.002 0 0 0-.175.016a13.096 13.096 0 0 0 1.825 25.981c.164.006.328 0 .49 0a13.072 13.072 0 0 0 10.703-5.555a1.01 1.01 0 0 0-.783-1.565A13.08 13.08 0 0 1 15.89 4.38A1.015 1.015 0 0 0 14.98 3z",fill:"currentColor"},null,-1),Ep=[yp],wp=ue({__name:"ToggleColorModeButton",setup(e){const t=ze(),n=Vo(),r=()=>{n.value=!n.value};return(o,s)=>(j(),X("button",{class:"toggle-color-mode-button",title:ee(t).toggleColorMode,onClick:r},[pr((j(),X("svg",gp,_p,512)),[[br,!ee(n)]]),pr((j(),X("svg",bp,Ep,512)),[[br,ee(n)]])],8,mp))}}),kp=Le(wp,[["__file","ToggleColorModeButton.vue"]]),xp=["title"],Lp=he("div",{class:"icon","aria-hidden":"true"},[he("span"),he("span"),he("span")],-1),Cp=[Lp],Tp=ue({__name:"ToggleSidebarButton",emits:["toggle"],setup(e){const t=ze();return(n,r)=>(j(),X("div",{class:"toggle-sidebar-button",title:ee(t).toggleSidebar,"aria-expanded":"false",role:"button",tabindex:"0",onClick:r[0]||(r[0]=o=>n.$emit("toggle"))},Cp,8,xp))}}),Sp=Le(Tp,[["__file","ToggleSidebarButton.vue"]]),Pp=ue({__name:"Navbar",emits:["toggle-sidebar"],setup(e){const t=ze(),n=me(null),r=me(null),o=me(0),s=F(()=>o.value?{maxWidth:o.value+"px"}:{});Wi(Hn.MOBILE,i=>{var c;const a=l(n.value,"paddingLeft")+l(n.value,"paddingRight");window.innerWidth{const c=bt("NavbarSearch");return j(),X("header",{ref_key:"navbar",ref:n,class:"navbar"},[te(Sp,{onToggle:a[0]||(a[0]=u=>i.$emit("toggle-sidebar"))}),he("span",{ref_key:"navbarBrand",ref:r},[te(Jh)],512),he("div",{class:"navbar-items-wrapper",style:jn(s.value)},[Ee(i.$slots,"before"),te(Ji,{class:"can-hide"}),Ee(i.$slots,"after"),ee(t).colorModeSwitch?(j(),Se(kp,{key:0})):Ce("v-if",!0),te(c)],4)],512)}}}),Ap=Le(Pp,[["__file","Navbar.vue"]]),Op={class:"page-meta"},Rp={key:0,class:"meta-item edit-link"},Ip={key:1,class:"meta-item last-updated"},$p={class:"meta-item-label"},Np={class:"meta-item-info"},Dp={key:2,class:"meta-item contributors"},Mp={class:"meta-item-label"},Hp={class:"meta-item-info"},Fp=["title"],jp=ue({__name:"PageMeta",setup(e){const t=()=>{const a=ze(),c=Gt(),u=gt();return F(()=>{if(!(u.value.editLink??a.value.editLink??!0))return null;const{repo:h,docsRepo:g=h,docsBranch:y="main",docsDir:w="",editLinkText:C}=a.value;if(!g)return null;const v=dp({docsRepo:g,docsBranch:y,docsDir:w,filePathRelative:c.value.filePathRelative,editLinkPattern:u.value.editLinkPattern??a.value.editLinkPattern});return v?{text:C??"Edit this page",link:v}:null})},n=()=>{const a=ze(),c=Gt(),u=gt();return F(()=>{var g,y;return!(u.value.lastUpdated??a.value.lastUpdated??!0)||!((g=c.value.git)!=null&&g.updatedTime)?null:new Date((y=c.value.git)==null?void 0:y.updatedTime).toLocaleString()})},r=()=>{const a=ze(),c=Gt(),u=gt();return F(()=>{var h;return u.value.contributors??a.value.contributors??!0?((h=c.value.git)==null?void 0:h.contributors)??null:null})},o=ze(),s=t(),l=n(),i=r();return(a,c)=>{const u=bt("ClientOnly");return j(),X("footer",Op,[ee(s)?(j(),X("div",Rp,[te(vt,{class:"meta-item-label",item:ee(s)},null,8,["item"])])):Ce("v-if",!0),ee(l)?(j(),X("div",Ip,[he("span",$p,Re(ee(o).lastUpdatedText)+": ",1),te(u,null,{default:De(()=>[he("span",Np,Re(ee(l)),1)]),_:1})])):Ce("v-if",!0),ee(i)&&ee(i).length?(j(),X("div",Dp,[he("span",Mp,Re(ee(o).contributorsText)+": ",1),he("span",Hp,[(j(!0),X(we,null,It(ee(i),(f,h)=>(j(),X(we,{key:h},[he("span",{class:"contributor",title:`email: ${f.email}`},Re(f.name),9,Fp),h!==ee(i).length-1?(j(),X(we,{key:0},[Nt(", ")],64)):Ce("v-if",!0)],64))),128))])])):Ce("v-if",!0)])}}}),Bp=Le(jp,[["__file","PageMeta.vue"]]),zp={key:0,class:"page-nav"},Vp={class:"inner"},Up={key:0,class:"prev"},qp={key:1,class:"next"},Wp=ue({__name:"PageNav",setup(e){const t=a=>a===!1?null:ge(a)?Uo(a):No(a)?a:!1,n=(a,c,u)=>{const f=a.findIndex(h=>h.link===c);if(f!==-1){const h=a[f+u];return h!=null&&h.link?h:null}for(const h of a)if(h.children){const g=n(h.children,c,u);if(g)return g}return null},r=gt(),o=qo(),s=Qt(),l=F(()=>{const a=t(r.value.prev);return a!==!1?a:n(o.value,s.path,-1)}),i=F(()=>{const a=t(r.value.next);return a!==!1?a:n(o.value,s.path,1)});return(a,c)=>l.value||i.value?(j(),X("nav",zp,[he("p",Vp,[l.value?(j(),X("span",Up,[te(vt,{item:l.value},null,8,["item"])])):Ce("v-if",!0),i.value?(j(),X("span",qp,[te(vt,{item:i.value},null,8,["item"])])):Ce("v-if",!0)])])):Ce("v-if",!0)}}),Kp=Le(Wp,[["__file","PageNav.vue"]]),Gp={class:"page"},Yp={class:"theme-default-content"},Jp=ue({__name:"Page",setup(e){return(t,n)=>{const r=bt("Content");return j(),X("main",Gp,[Ee(t.$slots,"top"),he("div",Yp,[Ee(t.$slots,"content-top"),te(r),Ee(t.$slots,"content-bottom")]),te(Bp),te(Kp),Ee(t.$slots,"bottom")])}}}),Qp=Le(Jp,[["__file","Page.vue"]]),Zp=["onKeydown"],Xp={class:"sidebar-item-children"},em=ue({__name:"SidebarItem",props:{item:{type:Object,required:!0},depth:{type:Number,required:!1,default:0}},setup(e){const t=e,{item:n,depth:r}=Tr(t),o=Qt(),s=Jt(),l=F(()=>Gi(n.value,o)),i=F(()=>({"sidebar-item":!0,"sidebar-heading":r.value===0,active:l.value,collapsible:n.value.collapsible})),a=F(()=>n.value.collapsible?l.value:!0),[c,u]=zd(a.value),f=g=>{n.value.collapsible&&(g.preventDefault(),u())},h=s.afterEach(g=>{Sr(()=>{c.value=a.value})});return Un(()=>{h()}),(g,y)=>{var C;const w=bt("SidebarItem",!0);return j(),X("li",null,[ee(n).link?(j(),Se(vt,{key:0,class:qe(i.value),item:ee(n)},null,8,["class","item"])):(j(),X("p",{key:1,tabindex:"0",class:qe(i.value),onClick:f,onKeydown:Au(f,["enter"])},[Nt(Re(ee(n).text)+" ",1),ee(n).collapsible?(j(),X("span",{key:0,class:qe(["arrow",ee(c)?"down":"right"])},null,2)):Ce("v-if",!0)],42,Zp)),(C=ee(n).children)!=null&&C.length?(j(),Se(Ki,{key:2},{default:De(()=>[pr(he("ul",Xp,[(j(!0),X(we,null,It(ee(n).children,v=>(j(),Se(w,{key:`${ee(r)}${v.text}${v.link}`,item:v,depth:ee(r)+1},null,8,["item","depth"]))),128))],512),[[br,ee(c)]])]),_:1})):Ce("v-if",!0)])}}}),tm=Le(em,[["__file","SidebarItem.vue"]]),nm={key:0,class:"sidebar-items"},rm=ue({__name:"SidebarItems",setup(e){const t=Qt(),n=qo();return Je(()=>{st(()=>t.hash,r=>{const o=document.querySelector(".sidebar");if(!o)return;const s=document.querySelector(`.sidebar a.sidebar-item[href="${t.path}${r}"]`);if(!s)return;const{top:l,height:i}=o.getBoundingClientRect(),{top:a,height:c}=s.getBoundingClientRect();al+i&&s.scrollIntoView(!1)})}),(r,o)=>ee(n).length?(j(),X("ul",nm,[(j(!0),X(we,null,It(ee(n),s=>(j(),Se(tm,{key:`${s.text}${s.link}`,item:s},null,8,["item"]))),128))])):Ce("v-if",!0)}}),om=Le(rm,[["__file","SidebarItems.vue"]]),sm={class:"sidebar"},lm=ue({__name:"Sidebar",setup(e){return(t,n)=>(j(),X("aside",sm,[te(Ji),Ee(t.$slots,"top"),te(om),Ee(t.$slots,"bottom")]))}}),im=Le(lm,[["__file","Sidebar.vue"]]),am=ue({__name:"Layout",setup(e){const t=Gt(),n=gt(),r=ze(),o=F(()=>n.value.navbar!==!1&&r.value.navbar!==!1),s=qo(),l=me(!1),i=C=>{l.value=typeof C=="boolean"?C:!l.value},a={x:0,y:0},c=C=>{a.x=C.changedTouches[0].clientX,a.y=C.changedTouches[0].clientY},u=C=>{const v=C.changedTouches[0].clientX-a.x,b=C.changedTouches[0].clientY-a.y;Math.abs(v)>Math.abs(b)&&Math.abs(v)>40&&(v>0&&a.x<=80?i(!0):i(!1))},f=F(()=>[{"no-navbar":!o.value,"no-sidebar":!s.value.length,"sidebar-open":l.value},n.value.pageClass]);let h;Je(()=>{h=Jt().afterEach(()=>{i(!1)})}),Rr(()=>{h()});const g=Vi(),y=g.resolve,w=g.pending;return(C,v)=>(j(),X("div",{class:qe(["theme-container",f.value]),onTouchstart:c,onTouchend:u},[Ee(C.$slots,"navbar",{},()=>[o.value?(j(),Se(Ap,{key:0,onToggleSidebar:i},{before:De(()=>[Ee(C.$slots,"navbar-before")]),after:De(()=>[Ee(C.$slots,"navbar-after")]),_:3})):Ce("v-if",!0)]),he("div",{class:"sidebar-mask",onClick:v[0]||(v[0]=b=>i(!1))}),Ee(C.$slots,"sidebar",{},()=>[te(im,null,{top:De(()=>[Ee(C.$slots,"sidebar-top")]),bottom:De(()=>[Ee(C.$slots,"sidebar-bottom")]),_:3})]),Ee(C.$slots,"page",{},()=>[ee(n).home?(j(),Se(Gh,{key:0})):(j(),Se(qn,{key:1,name:"fade-slide-y",mode:"out-in",onBeforeEnter:ee(y),onBeforeLeave:ee(w)},{default:De(()=>[(j(),Se(Qp,{key:ee(t).path},{top:De(()=>[Ee(C.$slots,"page-top")]),"content-top":De(()=>[Ee(C.$slots,"page-content-top")]),"content-bottom":De(()=>[Ee(C.$slots,"page-content-bottom")]),bottom:De(()=>[Ee(C.$slots,"page-bottom")]),_:3}))]),_:3},8,["onBeforeEnter","onBeforeLeave"]))])],34))}}),cm=Le(am,[["__file","Layout.vue"]]),um={class:"theme-container"},fm={class:"page"},dm={class:"theme-default-content"},hm=he("h1",null,"404",-1),pm=ue({__name:"NotFound",setup(e){const t=Kn(),n=ze(),r=n.value.notFound??["Not Found"],o=()=>r[Math.floor(Math.random()*r.length)],s=n.value.home??t.value,l=n.value.backToHome??"Back to home";return(i,a)=>{const c=bt("RouterLink");return j(),X("div",um,[he("main",fm,[he("div",dm,[hm,he("blockquote",null,Re(o()),1),te(c,{to:ee(s)},{default:De(()=>[Nt(Re(ee(l)),1)]),_:1},8,["to"])])])])}}}),mm=Le(pm,[["__file","NotFound.vue"]]);const gm=Dt({enhance({app:e,router:t}){e.component("Badge",Rd),e.component("CodeGroup",Id),e.component("CodeGroupItem",Md),e.component("AutoLinkExternalIcon",()=>{const r=e.component("ExternalLinkIcon");return r?ae(r):null}),e.component("NavbarSearch",()=>{const r=e.component("Docsearch")||e.component("SearchBox");return r?ae(r):null});const n=t.options.scrollBehavior;t.options.scrollBehavior=async(...r)=>(await Vi().wait(),n(...r))},setup(){mh(),_h()},layouts:{Layout:cm,NotFound:mm}}),vm=e=>e instanceof Element?document.activeElement===e&&(["TEXTAREA","SELECT","INPUT"].includes(e.tagName)||e.hasAttribute("contenteditable")):!1,_m=(e,t)=>t.some(n=>{if(ge(n))return n===e.key;const{key:r,ctrl:o=!1,shift:s=!1,alt:l=!1}=n;return r===e.key&&o===e.ctrlKey&&s===e.shiftKey&&l===e.altKey}),bm=/[^\x00-\x7F]/,ym=e=>e.split(/\s+/g).map(t=>t.trim()).filter(t=>!!t),cl=e=>e.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"),ul=(e,t)=>{const n=t.join(" "),r=ym(e);if(bm.test(e))return r.some(l=>n.toLowerCase().indexOf(l)>-1);const o=e.endsWith(" ");return new RegExp(r.map((l,i)=>r.length===i+1&&!o?`(?=.*\\b${cl(l)})`:`(?=.*\\b${cl(l)}\\b)`).join("")+".+","gi").test(n)},Em=({input:e,hotKeys:t})=>{if(t.value.length===0)return;const n=r=>{e.value&&_m(r,t.value)&&!vm(r.target)&&(r.preventDefault(),e.value.focus())};Je(()=>{document.addEventListener("keydown",n)}),Un(()=>{document.removeEventListener("keydown",n)})},wm=[{title:"",headers:[{level:2,title:"☘️Introduction",slug:"☘️introduction",link:"#☘️introduction",children:[]},{level:2,title:"📖使用说明",slug:"📖使用说明",link:"#📖使用说明",children:[{level:3,title:"1. 安装",slug:"_1-安装",link:"#_1-安装",children:[]},{level:3,title:"2. 开发",slug:"_2-开发",link:"#_2-开发",children:[]}]},{level:2,title:"部署",slug:"部署",link:"#部署",children:[]}],path:"/develop.html",pathLocale:"/",extraFields:[]},{title:"介绍",headers:[{level:2,title:"seldom 介绍",slug:"seldom-介绍",link:"#seldom-介绍",children:[]},{level:2,title:"seldom 理念",slug:"seldom-理念",link:"#seldom-理念",children:[]},{level:2,title:"seldom 历史",slug:"seldom-历史",link:"#seldom-历史",children:[]},{level:2,title:"seldom vs pytest",slug:"seldom-vs-pytest",link:"#seldom-vs-pytest",children:[]}],path:"/introduce.html",pathLocale:"/",extraFields:[]},{title:"",headers:[],path:"/",pathLocale:"/",extraFields:[]},{title:"appium API",headers:[{level:2,title:"appium 定位",slug:"appium-定位",link:"#appium-定位",children:[]},{level:2,title:"appium lab",slug:"appium-lab",link:"#appium-lab",children:[]},{level:2,title:"appium driver",slug:"appium-driver",link:"#appium-driver",children:[]}],path:"/app-testing/appium_lab.html",pathLocale:"/",extraFields:[]},{title:"appium 扩展",headers:[{level:2,title:"appium images-plugin",slug:"appium-images-plugin",link:"#appium-images-plugin",children:[]},{level:2,title:"Appium OCR plugin",slug:"appium-ocr-plugin",link:"#appium-ocr-plugin",children:[]}],path:"/app-testing/extensions.html",pathLocale:"/",extraFields:[]},{title:"Page Object",headers:[],path:"/app-testing/page_object.html",pathLocale:"/",extraFields:[]},{title:"app 测试",headers:[{level:2,title:"环境安装",slug:"环境安装",link:"#环境安装",children:[]},{level:2,title:"编写测试",slug:"编写测试",link:"#编写测试",children:[]}],path:"/app-testing/start.html",pathLocale:"/",extraFields:[]},{title:"支持Excel测试用例",headers:[{level:3,title:"编写Excel用例",slug:"编写excel用例",link:"#编写excel用例",children:[]},{level:3,title:"运行测试用例",slug:"运行测试用例",link:"#运行测试用例",children:[]}],path:"/api-testing/api_case.html",pathLocale:"/",extraFields:[]},{title:"API Object",headers:[],path:"/api-testing/api_object.html",pathLocale:"/",extraFields:[]},{title:"更强大的断言",headers:[{level:3,title:"assertJSON",slug:"assertjson",link:"#assertjson",children:[]},{level:3,title:"assertPath",slug:"assertpath",link:"#assertpath",children:[]},{level:3,title:"assertSchema",slug:"assertschema",link:"#assertschema",children:[]}],path:"/api-testing/assert.html",pathLocale:"/",extraFields:[]},{title:"更多功能",headers:[{level:3,title:"har to case",slug:"har-to-case",link:"#har-to-case",children:[]},{level:3,title:"swagger to case",slug:"swagger-to-case",link:"#swagger-to-case",children:[]},{level:3,title:"请求转 cURL",slug:"请求转-curl",link:"#请求转-curl",children:[]},{level:3,title:"接口数据依赖",slug:"接口数据依赖",link:"#接口数据依赖",children:[]},{level:3,title:"Session使用",slug:"session使用",link:"#session使用",children:[]},{level:3,title:"提取接口返回数据",slug:"提取接口返回数据",link:"#提取接口返回数据",children:[]},{level:3,title:"genson",slug:"genson",link:"#genson",children:[]},{level:3,title:"mock URL",slug:"mock-url",link:"#mock-url",children:[]},{level:3,title:"@retry装饰器",slug:"retry装饰器",link:"#retry装饰器",children:[]}],path:"/api-testing/more.html",pathLocale:"/",extraFields:[]},{title:"开始使用",headers:[{level:3,title:"前言",slug:"前言",link:"#前言",children:[]},{level:3,title:"Seldom VS Request+unittest",slug:"seldom-vs-request-unittest",link:"#seldom-vs-request-unittest",children:[]},{level:3,title:"运行测试",slug:"运行测试",link:"#运行测试",children:[]}],path:"/api-testing/start.html",pathLocale:"/",extraFields:[]},{title:"WebSocket",headers:[{level:3,title:"WebSocket 生命周期",slug:"websocket-生命周期",link:"#websocket-生命周期",children:[]},{level:3,title:"seldom测试WebSocket",slug:"seldom测试websocket",link:"#seldom测试websocket",children:[]}],path:"/api-testing/webscocket.html",pathLocale:"/",extraFields:[]},{title:"高级用法",headers:[{level:3,title:"fixture",slug:"fixture",link:"#fixture",children:[]},{level:3,title:"跳过测试",slug:"跳过测试",link:"#跳过测试",children:[]},{level:3,title:"重复执行",slug:"重复执行",link:"#重复执行",children:[]},{level:3,title:"随机测试数据",slug:"随机测试数据",link:"#随机测试数据",children:[]},{level:3,title:"用例的依赖",slug:"用例的依赖",link:"#用例的依赖",children:[]},{level:3,title:"用例分类标签",slug:"用例分类标签",link:"#用例分类标签",children:[]},{level:3,title:"发送邮件",slug:"发送邮件",link:"#发送邮件",children:[]},{level:3,title:"发送钉钉",slug:"发送钉钉",link:"#发送钉钉",children:[]},{level:3,title:"seldom日志",slug:"seldom日志",link:"#seldom日志",children:[]},{level:3,title:"缓存 cache",slug:"缓存-cache",link:"#缓存-cache",children:[]}],path:"/getting-started/advanced.html",pathLocale:"/",extraFields:[]},{title:"创建项目",headers:[{level:3,title:"自动生成项目",slug:"自动生成项目",link:"#自动生成项目",children:[]},{level:3,title:"创建测试用例",slug:"创建测试用例",link:"#创建测试用例",children:[]}],path:"/getting-started/create_project.html",pathLocale:"/",extraFields:[]},{title:"数据驱动",headers:[{level:3,title:"@class_data() 方法",slug:"class-data-方法",link:"#class-data-方法",children:[]},{level:3,title:"@data()方法",slug:"data-方法",link:"#data-方法",children:[]},{level:3,title:"@file_data() 方法",slug:"file-data-方法",link:"#file-data-方法",children:[]},{level:3,title:"@api_data()方法",slug:"api-data-方法",link:"#api-data-方法",children:[]},{level:3,title:"使用函数构造数据",slug:"使用函数构造数据",link:"#使用函数构造数据",children:[]},{level:3,title:"支持第三方 ddt 库",slug:"支持第三方-ddt-库",link:"#支持第三方-ddt-库",children:[]}],path:"/getting-started/data_driver.html",pathLocale:"/",extraFields:[]},{title:"方法的依赖",headers:[{level:3,title:"类内部方法调用",slug:"类内部方法调用",link:"#类内部方法调用",children:[]},{level:3,title:"外部类方法依赖",slug:"外部类方法依赖",link:"#外部类方法依赖",children:[]},{level:3,title:"多重方法依赖",slug:"多重方法依赖",link:"#多重方法依赖",children:[]},{level:3,title:"参数化使用",slug:"参数化使用",link:"#参数化使用",children:[]}],path:"/getting-started/dependent_func.html",pathLocale:"/",extraFields:[]},{title:"Installation",headers:[],path:"/getting-started/installation.html",pathLocale:"/",extraFields:[]},{title:"快速开始",headers:[{level:3,title:"基本规范",slug:"基本规范",link:"#基本规范",children:[]},{level:3,title:"main() 方法",slug:"main-方法",link:"#main-方法",children:[]},{level:3,title:"confrun.py 配置文件",slug:"confrun-py-配置文件",link:"#confrun-py-配置文件",children:[]},{level:3,title:"运行测试",slug:"运行测试",link:"#运行测试",children:[]},{level:3,title:"失败重跑",slug:"失败重跑",link:"#失败重跑",children:[]},{level:3,title:"测试报告",slug:"测试报告",link:"#测试报告",children:[]},{level:3,title:"多线程运行",slug:"多线程运行",link:"#多线程运行",children:[]}],path:"/getting-started/quick_start.html",pathLocale:"/",extraFields:[]},{title:"seldom CLI",headers:[{level:2,title:"seldom 帮助",slug:"seldom-帮助",link:"#seldom-帮助",children:[]},{level:2,title:"seldom 使用",slug:"seldom-使用",link:"#seldom-使用",children:[{level:3,title:"创建项目",slug:"创建项目",link:"#创建项目",children:[]},{level:3,title:"生成接口自动化用例",slug:"生成接口自动化用例",link:"#生成接口自动化用例",children:[]},{level:3,title:"运行测试目录&文件",slug:"运行测试目录-文件",link:"#运行测试目录-文件",children:[]},{level:3,title:"运行文件&类&方法",slug:"运行文件-类-方法",link:"#运行文件-类-方法",children:[]},{level:3,title:"调试模式",slug:"调试模式",link:"#调试模式",children:[]},{level:3,title:"运行浏览器",slug:"运行浏览器",link:"#运行浏览器",children:[]},{level:3,title:"运行URL",slug:"运行url",link:"#运行url",children:[]},{level:3,title:"测试报告",slug:"测试报告",link:"#测试报告",children:[]},{level:3,title:"失败/错误重跑次数",slug:"失败-错误重跑次数",link:"#失败-错误重跑次数",children:[]},{level:3,title:"数据驱动运行环境",slug:"数据驱动运行环境",link:"#数据驱动运行环境",children:[]},{level:3,title:"收集测试用例",slug:"收集测试用例",link:"#收集测试用例",children:[]},{level:3,title:"运行收集测试用例",slug:"运行收集测试用例",link:"#运行收集测试用例",children:[]},{level:3,title:"清除所有缓存",slug:"清除所有缓存",link:"#清除所有缓存",children:[]},{level:3,title:"执行 API(excel文件)测试用例",slug:"执行-api-excel文件-测试用例",link:"#执行-api-excel文件-测试用例",children:[]}]}],path:"/getting-started/seldom_cli.html",pathLocale:"/",extraFields:[]},{title:"平台化支持",headers:[{level:3,title:"获取用例信息",slug:"获取用例信息",link:"#获取用例信息",children:[]},{level:3,title:"执行用例信息",slug:"执行用例信息",link:"#执行用例信息",children:[]},{level:3,title:"相关项目",slug:"相关项目",link:"#相关项目",children:[]}],path:"/platform/platform.html",pathLocale:"/",extraFields:[]},{title:"数据库操作",headers:[{level:3,title:"连接数据库",slug:"连接数据库",link:"#连接数据库",children:[]},{level:3,title:"操作方法",slug:"操作方法",link:"#操作方法",children:[]},{level:2,title:"MongoDB",slug:"mongodb",link:"#mongodb",children:[]}],path:"/more-ability/db_operation.html",pathLocale:"/",extraFields:[]},{title:"支持更多测试库",headers:[{level:3,title:"使用playwright",slug:"使用playwright",link:"#使用playwright",children:[]},{level:3,title:"使用uiautomator2",slug:"使用uiautomator2",link:"#使用uiautomator2",children:[]},{level:3,title:"使用pyAutoGUI",slug:"使用pyautogui",link:"#使用pyautogui",children:[]}],path:"/more-ability/test_library.html",pathLocale:"/",extraFields:[]},{title:"浏览器与驱动",headers:[{level:3,title:"管理浏览器驱动",slug:"管理浏览器驱动",link:"#管理浏览器驱动",children:[]},{level:3,title:"指定浏览器驱动",slug:"指定浏览器驱动",link:"#指定浏览器驱动",children:[]},{level:3,title:"指定不同的浏览器",slug:"指定不同的浏览器",link:"#指定不同的浏览器",children:[]},{level:3,title:"控制浏览器启动和关闭",slug:"控制浏览器启动和关闭",link:"#控制浏览器启动和关闭",children:[]}],path:"/web-testing/browser_driver.html",pathLocale:"/",extraFields:[]},{title:"链式调用",headers:[{level:3,title:"基本例子",slug:"基本例子",link:"#基本例子",children:[]},{level:3,title:"Steps 类",slug:"steps-类",link:"#steps-类",children:[]},{level:3,title:"控制浏览器启动和关闭",slug:"控制浏览器启动和关闭",link:"#控制浏览器启动和关闭",children:[]}],path:"/web-testing/chaining.html",pathLocale:"/",extraFields:[]},{title:"浏览器启动配置",headers:[{level:3,title:"使用headless模式",slug:"使用headless模式",link:"#使用headless模式",children:[]},{level:3,title:"Selenium Grid",slug:"selenium-grid",link:"#selenium-grid",children:[]},{level:3,title:"Mobile Web 模式",slug:"mobile-web-模式",link:"#mobile-web-模式",children:[]},{level:3,title:"浏览器忽略无效证书",slug:"浏览器忽略无效证书",link:"#浏览器忽略无效证书",children:[]},{level:3,title:"浏览器关闭沙盒模式",slug:"浏览器关闭沙盒模式",link:"#浏览器关闭沙盒模式",children:[]},{level:3,title:"开启实验性功能",slug:"开启实验性功能",link:"#开启实验性功能",children:[]},{level:3,title:"设置浏览器代理",slug:"设置浏览器代理",link:"#设置浏览器代理",children:[]},{level:3,title:"连接已打开浏览器",slug:"连接已打开浏览器",link:"#连接已打开浏览器",children:[]}],path:"/web-testing/other.html",pathLocale:"/",extraFields:[]},{title:"Page Object",headers:[],path:"/web-testing/page_object.html",pathLocale:"/",extraFields:[]},{title:"Seldom API",headers:[{level:3,title:"查找元素",slug:"查找元素",link:"#查找元素",children:[]},{level:3,title:"断言",slug:"断言",link:"#断言",children:[]},{level:3,title:"WebDriver API",slug:"webdriver-api",link:"#webdriver-api",children:[]},{level:3,title:"键盘操作",slug:"键盘操作",link:"#键盘操作",children:[]},{level:3,title:"测试electron应用",slug:"测试electron应用",link:"#测试electron应用",children:[]}],path:"/web-testing/seldom_api.html",pathLocale:"/",extraFields:[]},{title:"版本更新",headers:[{level:3,title:"seldom 3.x",slug:"seldom-3-x",link:"#seldom-3-x",children:[]},{level:3,title:"seldom 2.x",slug:"seldom-2-x",link:"#seldom-2-x",children:[]},{level:3,title:"seldom 1.x",slug:"seldom-1-x",link:"#seldom-1-x",children:[]},{level:3,title:"seldom 0.x",slug:"seldom-0-x",link:"#seldom-0-x",children:[]},{level:3,title:"pyse",slug:"pyse",link:"#pyse",children:[]}],path:"/version/CHANGES.html",pathLocale:"/",extraFields:[]},{title:"",headers:[],path:"/404.html",pathLocale:"/",extraFields:[]}],km=me(wm),xm=()=>km,Lm=({searchIndex:e,routeLocale:t,query:n,maxSuggestions:r})=>{const o=F(()=>e.value.filter(s=>s.pathLocale===t.value));return F(()=>{const s=n.value.trim().toLowerCase();if(!s)return[];const l=[],i=(a,c)=>{ul(s,[c.title])&&l.push({link:`${a.path}#${c.slug}`,title:a.title,header:c.title});for(const u of c.children){if(l.length>=r.value)return;i(a,u)}};for(const a of o.value){if(l.length>=r.value)break;if(ul(s,[a.title,...a.extraFields])){l.push({link:a.path,title:a.title});continue}for(const c of a.headers){if(l.length>=r.value)break;i(a,c)}}return l})},Cm=e=>{const t=me(0);return{focusIndex:t,focusNext:()=>{t.value{t.value>0?t.value-=1:t.value=e.value.length-1}}},Tm=ue({name:"SearchBox",props:{locales:{type:Object,required:!1,default:()=>({})},hotKeys:{type:Array,required:!1,default:()=>[]},maxSuggestions:{type:Number,required:!1,default:5}},setup(e){const{locales:t,hotKeys:n,maxSuggestions:r}=Tr(e),o=Jt(),s=Kn(),l=xm(),i=me(null),a=me(!1),c=me(""),u=F(()=>t.value[s.value]??{}),f=Lm({searchIndex:l,routeLocale:s,query:c,maxSuggestions:r}),{focusIndex:h,focusNext:g,focusPrev:y}=Cm(f);Em({input:i,hotKeys:n});const w=F(()=>a.value&&!!f.value.length),C=()=>{w.value&&y()},v=()=>{w.value&&g()},b=P=>{if(!w.value)return;const S=f.value[P];S&&o.push(S.link).then(()=>{c.value="",h.value=0})};return()=>ae("form",{class:"search-box",role:"search"},[ae("input",{ref:i,type:"search",placeholder:u.value.placeholder,autocomplete:"off",spellcheck:!1,value:c.value,onFocus:()=>a.value=!0,onBlur:()=>a.value=!1,onInput:P=>c.value=P.target.value,onKeydown:P=>{switch(P.key){case"ArrowUp":{C();break}case"ArrowDown":{v();break}case"Enter":{P.preventDefault(),b(h.value);break}}}}),w.value&&ae("ul",{class:"suggestions",onMouseleave:()=>h.value=-1},f.value.map(({link:P,title:S,header:U},Z)=>ae("li",{class:["suggestion",{focus:h.value===Z}],onMouseenter:()=>h.value=Z,onMousedown:()=>b(Z)},ae("a",{href:P,onClick:$=>$.preventDefault()},[ae("span",{class:"page-title"},S),U&&ae("span",{class:"page-header"},`> ${U}`)]))))])}});const Sm={"/":{placeholder:"Search"},"/zh/":{placeholder:"搜索"}},Pm=["s","/"],Am=5,Om=Dt({enhance({app:e}){e.component("SearchBox",t=>ae(Tm,{locales:Sm,hotKeys:Pm,maxSuggestions:Am,...t}))}}),fl=()=>window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0,Rm=()=>window.scrollTo({top:0,behavior:"smooth"});const Im=ue({name:"BackToTop",setup(){const e=me(0),t=F(()=>e.value>300),n=Ei(()=>{e.value=fl()},100);Je(()=>{e.value=fl(),window.addEventListener("scroll",()=>n())});const r=ae("div",{class:"back-to-top",onClick:Rm});return()=>ae(qn,{name:"back-to-top"},()=>t.value?r:null)}}),$m=Dt({rootComponents:[Im]}),lr=[sd,cd,Ed,Ld,Ad,gm,Om,$m],Nm=[["v-fe360bd6","/develop.html",{title:""},["/develop","/develop.md"]],["v-d08d435a","/introduce.html",{title:"介绍"},["/introduce","/introduce.md"]],["v-8daa1a0e","/",{title:""},["/index.html","/README.md"]],["v-53c50d87","/app-testing/appium_lab.html",{title:"appium API"},["/app-testing/appium_lab","/app-testing/appium_lab.md"]],["v-483ce5fe","/app-testing/extensions.html",{title:"appium 扩展"},["/app-testing/extensions","/app-testing/extensions.md"]],["v-299549e4","/app-testing/page_object.html",{title:"Page Object"},["/app-testing/page_object","/app-testing/page_object.md"]],["v-4370387b","/app-testing/start.html",{title:"app 测试"},["/app-testing/start","/app-testing/start.md"]],["v-7cbb39b9","/api-testing/api_case.html",{title:"支持Excel测试用例"},["/api-testing/api_case","/api-testing/api_case.md"]],["v-109501ec","/api-testing/api_object.html",{title:"API Object"},["/api-testing/api_object","/api-testing/api_object.md"]],["v-d24a86f0","/api-testing/assert.html",{title:"更强大的断言"},["/api-testing/assert","/api-testing/assert.md"]],["v-5b62ef19","/api-testing/more.html",{title:"更多功能"},["/api-testing/more","/api-testing/more.md"]],["v-23f9483c","/api-testing/start.html",{title:"开始使用"},["/api-testing/start","/api-testing/start.md"]],["v-488b8cec","/api-testing/webscocket.html",{title:"WebSocket"},["/api-testing/webscocket","/api-testing/webscocket.md"]],["v-d8f79a72","/getting-started/advanced.html",{title:"高级用法"},["/getting-started/advanced","/getting-started/advanced.md"]],["v-8e520eda","/getting-started/create_project.html",{title:"创建项目"},["/getting-started/create_project","/getting-started/create_project.md"]],["v-78c619cc","/getting-started/data_driver.html",{title:"数据驱动"},["/getting-started/data_driver","/getting-started/data_driver.md"]],["v-4e6f0425","/getting-started/dependent_func.html",{title:"方法的依赖"},["/getting-started/dependent_func","/getting-started/dependent_func.md"]],["v-4e8563af","/getting-started/installation.html",{title:"Installation"},["/getting-started/installation","/getting-started/installation.md"]],["v-0f898c79","/getting-started/quick_start.html",{title:"快速开始"},["/getting-started/quick_start","/getting-started/quick_start.md"]],["v-6f32df80","/getting-started/seldom_cli.html",{title:"seldom CLI"},["/getting-started/seldom_cli","/getting-started/seldom_cli.md"]],["v-3cdc5c3a","/platform/platform.html",{title:"平台化支持"},["/platform/platform","/platform/platform.md"]],["v-c350a662","/more-ability/db_operation.html",{title:"数据库操作"},["/more-ability/db_operation","/more-ability/db_operation.md"]],["v-1d715ea7","/more-ability/test_library.html",{title:"支持更多测试库"},["/more-ability/test_library","/more-ability/test_library.md"]],["v-471218ee","/web-testing/browser_driver.html",{title:"浏览器与驱动"},["/web-testing/browser_driver","/web-testing/browser_driver.md"]],["v-198befa7","/web-testing/chaining.html",{title:"链式调用"},["/web-testing/chaining","/web-testing/chaining.md"]],["v-1f2c830c","/web-testing/other.html",{title:"浏览器启动配置"},["/web-testing/other","/web-testing/other.md"]],["v-0ccdb93b","/web-testing/page_object.html",{title:"Page Object"},["/web-testing/page_object","/web-testing/page_object.md"]],["v-40ed12b6","/web-testing/seldom_api.html",{title:"Seldom API"},["/web-testing/seldom_api","/web-testing/seldom_api.md"]],["v-129a7066","/version/CHANGES.html",{title:"版本更新"},["/version/CHANGES","/version/CHANGES.md"]],["v-3706649a","/404.html",{title:""},["/404"]]];var dl=ue({name:"Vuepress",setup(){const e=Wu();return()=>ae(e.value)}}),Dm=()=>Nm.reduce((e,[t,n,r,o])=>(e.push({name:t,path:n,component:dl,meta:r},...o.map(s=>({path:s,redirect:n}))),e),[{name:"404",path:"/:catchAll(.*)",component:dl}]),Mm=mf,Hm=()=>{const e=Zf({history:Mm(ui("/")),routes:Dm(),scrollBehavior:(t,n,r)=>r||(t.hash?{el:t.hash}:{top:0})});return e.beforeResolve(async(t,n)=>{var r;(t.path!==n.path||n===pt)&&([St.value]=await Promise.all([ht.resolvePageData(t.name),(r=hi[t.name])==null?void 0:r.__asyncLoader()]))}),e},Fm=e=>{e.component("ClientOnly",Ho),e.component("Content",Ju)},jm=(e,t,n)=>{const r=me(t.currentRoute.value.path);st(()=>t.currentRoute.value.path,h=>r.value=h);const o=F(()=>ht.resolveLayouts(n)),s=F(()=>ht.resolveRouteLocale(nn.value.locales,r.value)),l=F(()=>ht.resolveSiteLocaleData(nn.value,s.value)),i=F(()=>ht.resolvePageFrontmatter(St.value)),a=F(()=>ht.resolvePageHeadTitle(St.value,l.value)),c=F(()=>ht.resolvePageHead(a.value,i.value,l.value)),u=F(()=>ht.resolvePageLang(St.value)),f=F(()=>ht.resolvePageLayout(St.value,o.value));return e.provide(Bu,o),e.provide(mi,i),e.provide(Uu,a),e.provide(gi,c),e.provide(vi,u),e.provide(_i,f),e.provide(Do,s),e.provide(yi,l),Object.defineProperties(e.config.globalProperties,{$frontmatter:{get:()=>i.value},$head:{get:()=>c.value},$headTitle:{get:()=>a.value},$lang:{get:()=>u.value},$page:{get:()=>St.value},$routeLocale:{get:()=>s.value},$site:{get:()=>nn.value},$siteLocale:{get:()=>l.value},$withBase:{get:()=>Fo}}),{layouts:o,pageData:St,pageFrontmatter:i,pageHead:c,pageHeadTitle:a,pageLang:u,pageLayout:f,routeLocale:s,siteData:nn,siteLocaleData:l}},Bm=()=>{const e=Vu(),t=qu(),n=me([]),r=()=>{e.value.forEach(s=>{const l=zm(s);l&&n.value.push(l)})},o=()=>{document.documentElement.lang=t.value,n.value.forEach(s=>{s.parentNode===document.head&&document.head.removeChild(s)}),n.value.splice(0,n.value.length),e.value.forEach(s=>{const l=Vm(s);l!==null&&(document.head.appendChild(l),n.value.push(l))})};Wt(Ku,o),Je(()=>{r(),o(),st(()=>e.value,()=>o())})},zm=([e,t,n=""])=>{const r=Object.entries(t).map(([i,a])=>ge(a)?`[${i}=${JSON.stringify(a)}]`:a===!0?`[${i}]`:"").join(""),o=`head > ${e}${r}`;return Array.from(document.querySelectorAll(o)).find(i=>i.innerText===n)||null},Vm=([e,t,n])=>{if(!ge(e))return null;const r=document.createElement(e);return No(t)&&Object.entries(t).forEach(([o,s])=>{ge(s)?r.setAttribute(o,s):s===!0&&r.setAttribute(o,"")}),ge(n)&&r.appendChild(document.createTextNode(n)),r},Um=Iu,qm=async()=>{var n;const e=Um({name:"VuepressApp",setup(){var r;Bm();for(const o of lr)(r=o.setup)==null||r.call(o);return()=>[ae(Ri),...lr.flatMap(({rootComponents:o=[]})=>o.map(s=>ae(s)))]}}),t=Hm();Fm(e),jm(e,t,lr);for(const r of lr)await((n=r.enhance)==null?void 0:n.call(r,{app:e,router:t,siteData:nn}));return e.use(t),{app:e,router:t}};qm().then(({app:e,router:t})=>{t.isReady().then(()=>{e.mount("#app")})});export{Le as _,he as a,Nt as b,X as c,qm as createVueApp,te as d,Gc as e,j as o,bt as r}; + */const fe={settings:{minimum:.08,easing:"ease",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,barSelector:'[role="bar"]',parent:"body",template:'
    '},status:null,set:e=>{const t=fe.isStarted();e=Jr(e,fe.settings.minimum,1),fe.status=e===1?null:e;const n=fe.render(!t),r=n.querySelector(fe.settings.barSelector),o=fe.settings.speed,s=fe.settings.easing;return n.offsetWidth,wd(l=>{rr(r,{transform:"translate3d("+Zs(e)+"%,0,0)",transition:"all "+o+"ms "+s}),e===1?(rr(n,{transition:"none",opacity:"1"}),n.offsetWidth,setTimeout(function(){rr(n,{transition:"all "+o+"ms linear",opacity:"0"}),setTimeout(function(){fe.remove(),l()},o)},o)):setTimeout(()=>l(),o)}),fe},isStarted:()=>typeof fe.status=="number",start:()=>{fe.status||fe.set(0);const e=()=>{setTimeout(()=>{fe.status&&(fe.trickle(),e())},fe.settings.trickleSpeed)};return fe.settings.trickle&&e(),fe},done:e=>!e&&!fe.status?fe:fe.inc(.3+.5*Math.random()).set(1),inc:e=>{let t=fe.status;return t?(typeof e!="number"&&(e=(1-t)*Jr(Math.random()*t,.1,.95)),t=Jr(t+e,0,.994),fe.set(t)):fe.start()},trickle:()=>fe.inc(Math.random()*fe.settings.trickleRate),render:e=>{if(fe.isRendered())return document.getElementById("nprogress");Xs(document.documentElement,"nprogress-busy");const t=document.createElement("div");t.id="nprogress",t.innerHTML=fe.settings.template;const n=t.querySelector(fe.settings.barSelector),r=e?"-100":Zs(fe.status||0),o=document.querySelector(fe.settings.parent);return rr(n,{transition:"all 0 linear",transform:"translate3d("+r+"%,0,0)"}),o!==document.body&&Xs(o,"nprogress-custom-parent"),o==null||o.appendChild(t),t},remove:()=>{el(document.documentElement,"nprogress-busy"),el(document.querySelector(fe.settings.parent),"nprogress-custom-parent");const e=document.getElementById("nprogress");e&&kd(e)},isRendered:()=>!!document.getElementById("nprogress")},Jr=(e,t,n)=>en?n:e,Zs=e=>(-1+e)*100,wd=function(){const e=[];function t(){const n=e.shift();n&&n(t)}return function(n){e.push(n),e.length===1&&t()}}(),rr=function(){const e=["Webkit","O","Moz","ms"],t={};function n(l){return l.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,function(i,a){return a.toUpperCase()})}function r(l){const i=document.body.style;if(l in i)return l;let a=e.length;const c=l.charAt(0).toUpperCase()+l.slice(1);let u;for(;a--;)if(u=e[a]+c,u in i)return u;return l}function o(l){return l=n(l),t[l]??(t[l]=r(l))}function s(l,i,a){i=o(i),l.style[i]=a}return function(l,i){for(const a in i){const c=i[a];c!==void 0&&Object.prototype.hasOwnProperty.call(i,a)&&s(l,a,c)}}}(),Ii=(e,t)=>(typeof e=="string"?e:zo(e)).indexOf(" "+t+" ")>=0,Xs=(e,t)=>{const n=zo(e),r=n+t;Ii(n,t)||(e.className=r.substring(1))},el=(e,t)=>{const n=zo(e);if(!Ii(e,t))return;const r=n.replace(" "+t+" "," ");e.className=r.substring(1,r.length-1)},zo=e=>(" "+(e.className||"")+" ").replace(/\s+/gi," "),kd=e=>{e&&e.parentNode&&e.parentNode.removeChild(e)};const xd=()=>{Je(()=>{const e=Jt(),t=new Set;t.add(e.currentRoute.value.path),e.beforeEach(n=>{t.has(n.path)||fe.start()}),e.afterEach(n=>{t.add(n.path),fe.done()})})},Ld=Dt({setup(){xd()}}),Cd=JSON.parse(`{"repo":"SeldomQA/seldom","docsBranch":"vuepress-docs/docs/vpdocs","logo":"/logo.jpeg","navbar":[{"text":"介绍","link":"/introduce"},{"text":"安装","link":"/getting-started/installation"}],"sidebar":["/introduce",{"text":"开始","children":["/getting-started/installation","/getting-started/create_project","/getting-started/quick_start","/getting-started/advanced","/getting-started/data_driver","/getting-started/dependent_func","/getting-started/seldom_cli"]},{"text":"web UI 测试","children":["/web-testing/browser_driver","/web-testing/seldom_api","/web-testing/chaining","/web-testing/page_object","/web-testing/other"]},{"text":"App UI 测试","children":["/app-testing/start","/app-testing/appium_lab","/app-testing/page_object","/app-testing/extensions"]},{"text":"HTTP接口测试","children":["/api-testing/start","/api-testing/assert","/api-testing/api_object","/api-testing/more","/api-testing/api_case","/api-testing/webscocket"]},{"text":"更多能力","children":["/more-ability/db_operation","/more-ability/test_library"]},"/platform/platform","/version/CHANGES"],"editLinks":true,"editLinkText":"在 GitHub 上编辑此页","lastUpdated":"上次更新","locales":{"/":{"selectLanguageName":"English"}},"colorMode":"auto","colorModeSwitch":true,"selectLanguageText":"Languages","selectLanguageAriaLabel":"Select language","sidebarDepth":2,"editLink":true,"lastUpdatedText":"Last Updated","contributors":true,"contributorsText":"Contributors","notFound":["There's nothing here.","How did we get here?","That's a Four-Oh-Four.","Looks like we've got some broken links."],"backToHome":"Take me home","openInNewWindow":"open in new window","toggleColorMode":"toggle color mode","toggleSidebar":"toggle sidebar"}`),Td=me(Cd),$i=()=>Td,Ni=Symbol(""),Sd=()=>{const e=Pe(Ni);if(!e)throw new Error("useThemeLocaleData() is called without provider.");return e},Pd=(e,t)=>{const{locales:n,...r}=e;return{...r,...n==null?void 0:n[t]}},Ad=Dt({enhance({app:e}){const t=$i(),n=e._context.provides[Do],r=F(()=>Pd(t.value,n.value));e.provide(Ni,r),Object.defineProperties(e.config.globalProperties,{$theme:{get(){return t.value}},$themeLocale:{get(){return r.value}}})}}),Od=ue({__name:"Badge",props:{type:{type:String,required:!1,default:"tip"},text:{type:String,required:!1,default:""},vertical:{type:String,required:!1,default:void 0}},setup(e){return(t,n)=>(j(),X("span",{class:qe(["badge",e.type]),style:jn({verticalAlign:e.vertical})},[Ee(t.$slots,"default",{},()=>[Nt(Re(e.text),1)])],6))}}),Le=(e,t)=>{const n=e.__vccOpts||e;for(const[r,o]of t)n[r]=o;return n},Rd=Le(Od,[["__file","Badge.vue"]]),Id=ue({name:"CodeGroup",slots:Object,setup(e,{slots:t}){const n=me(-1),r=me([]),o=(i=n.value)=>{i{i>0?n.value=i-1:n.value=r.value.length-1,r.value[n.value].focus()},l=(i,a)=>{i.key===" "||i.key==="Enter"?(i.preventDefault(),n.value=a):i.key==="ArrowRight"?(i.preventDefault(),o(a)):i.key==="ArrowLeft"&&(i.preventDefault(),s(a))};return()=>{var a;const i=(((a=t.default)==null?void 0:a.call(t))||[]).filter(c=>c.type.name==="CodeGroupItem").map(c=>(c.props===null&&(c.props={}),c));return i.length===0?null:(n.value<0||n.value>i.length-1?(n.value=i.findIndex(c=>c.props.active===""||c.props.active===!0),n.value===-1&&(n.value=0)):i.forEach((c,u)=>{c.props.active=u===n.value}),ae("div",{class:"code-group"},[ae("div",{class:"code-group__nav"},ae("ul",{class:"code-group__ul"},i.map((c,u)=>{const f=u===n.value;return ae("li",{class:"code-group__li"},ae("button",{ref:h=>{h&&(r.value[u]=h)},class:{"code-group__nav-tab":!0,"code-group__nav-tab-active":f},ariaPressed:f,ariaExpanded:f,onClick:()=>n.value=u,onKeydown:h=>l(h,u)},c.props.title))}))),i]))}}}),$d=["aria-selected"],Nd=ue({name:"CodeGroupItem"}),Dd=ue({...Nd,props:{title:{type:String,required:!0},active:{type:Boolean,required:!1,default:!1}},setup(e){return(t,n)=>(j(),X("div",{class:qe(["code-group-item",{"code-group-item__active":e.active}]),"aria-selected":e.active},[Ee(t.$slots,"default")],10,$d))}}),Md=Le(Dd,[["__file","CodeGroupItem.vue"]]);function Di(e){return yl()?(ma(e),!0):!1}function Mn(e){return typeof e=="function"?e():ee(e)}const Hd=typeof window<"u",Mi=()=>{};function Fd(e,t){function n(...r){return new Promise((o,s)=>{Promise.resolve(e(()=>t.apply(this,r),{fn:t,thisArg:this,args:r})).then(o).catch(s)})}return n}const Hi=e=>e();function jd(e=Hi){const t=me(!0);function n(){t.value=!1}function r(){t.value=!0}const o=(...s)=>{t.value&&e(...s)};return{isActive:Bn(t),pause:n,resume:r,eventFilter:o}}function Bd(...e){if(e.length!==1)return Ja(...e);const t=e[0];return typeof t=="function"?Bn(Ka(()=>({get:t,set:Mi}))):me(t)}function zd(e=!1,t={}){const{truthyValue:n=!0,falsyValue:r=!1}=t,o=Ie(e),s=me(e);function l(i){if(arguments.length)return s.value=i,s.value;{const a=Mn(n);return s.value=s.value===a?Mn(r):a,s.value}}return o?l:[s,l]}var tl=Object.getOwnPropertySymbols,Vd=Object.prototype.hasOwnProperty,Ud=Object.prototype.propertyIsEnumerable,qd=(e,t)=>{var n={};for(var r in e)Vd.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(e!=null&&tl)for(var r of tl(e))t.indexOf(r)<0&&Ud.call(e,r)&&(n[r]=e[r]);return n};function Wd(e,t,n={}){const r=n,{eventFilter:o=Hi}=r,s=qd(r,["eventFilter"]);return st(e,Fd(o,t),s)}var Kd=Object.defineProperty,Gd=Object.defineProperties,Yd=Object.getOwnPropertyDescriptors,Er=Object.getOwnPropertySymbols,Fi=Object.prototype.hasOwnProperty,ji=Object.prototype.propertyIsEnumerable,nl=(e,t,n)=>t in e?Kd(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,Jd=(e,t)=>{for(var n in t||(t={}))Fi.call(t,n)&&nl(e,n,t[n]);if(Er)for(var n of Er(t))ji.call(t,n)&&nl(e,n,t[n]);return e},Qd=(e,t)=>Gd(e,Yd(t)),Zd=(e,t)=>{var n={};for(var r in e)Fi.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(e!=null&&Er)for(var r of Er(e))t.indexOf(r)<0&&ji.call(e,r)&&(n[r]=e[r]);return n};function Xd(e,t,n={}){const r=n,{eventFilter:o}=r,s=Zd(r,["eventFilter"]),{eventFilter:l,pause:i,resume:a,isActive:c}=jd(o);return{stop:Wd(e,t,Qd(Jd({},s),{eventFilter:l})),pause:i,resume:a,isActive:c}}function eh(e){var t;const n=Mn(e);return(t=n==null?void 0:n.$el)!=null?t:n}const wr=Hd?window:void 0;function rl(...e){let t,n,r,o;if(typeof e[0]=="string"||Array.isArray(e[0])?([n,r,o]=e,t=wr):[t,n,r,o]=e,!t)return Mi;Array.isArray(n)||(n=[n]),Array.isArray(r)||(r=[r]);const s=[],l=()=>{s.forEach(u=>u()),s.length=0},i=(u,f,h,g)=>(u.addEventListener(f,h,g),()=>u.removeEventListener(f,h,g)),a=st(()=>[eh(t),Mn(o)],([u,f])=>{l(),u&&s.push(...n.flatMap(h=>r.map(g=>i(u,h,g,f))))},{immediate:!0,flush:"post"}),c=()=>{a(),l()};return Di(c),c}function th(){const e=me(!1);return li()&&Je(()=>{e.value=!0}),e}function nh(e){const t=th();return F(()=>(t.value,!!e()))}function rh(e,t={}){const{window:n=wr}=t,r=nh(()=>n&&"matchMedia"in n&&typeof n.matchMedia=="function");let o;const s=me(!1),l=()=>{o&&("removeEventListener"in o?o.removeEventListener("change",i):o.removeListener(i))},i=()=>{r.value&&(l(),o=n.matchMedia(Bd(e).value),s.value=!!(o!=null&&o.matches),o&&("addEventListener"in o?o.addEventListener("change",i):o.addListener(i)))};return uc(i),Di(()=>l()),s}const or=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},sr="__vueuse_ssr_handlers__",oh=sh();function sh(){return sr in or||(or[sr]=or[sr]||{}),or[sr]}function lh(e,t){return oh[e]||t}function ih(e){return e==null?"any":e instanceof Set?"set":e instanceof Map?"map":e instanceof Date?"date":typeof e=="boolean"?"boolean":typeof e=="string"?"string":typeof e=="object"?"object":Number.isNaN(e)?"any":"number"}var ah=Object.defineProperty,ol=Object.getOwnPropertySymbols,ch=Object.prototype.hasOwnProperty,uh=Object.prototype.propertyIsEnumerable,sl=(e,t,n)=>t in e?ah(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,ll=(e,t)=>{for(var n in t||(t={}))ch.call(t,n)&&sl(e,n,t[n]);if(ol)for(var n of ol(t))uh.call(t,n)&&sl(e,n,t[n]);return e};const fh={boolean:{read:e=>e==="true",write:e=>String(e)},object:{read:e=>JSON.parse(e),write:e=>JSON.stringify(e)},number:{read:e=>Number.parseFloat(e),write:e=>String(e)},any:{read:e=>e,write:e=>String(e)},string:{read:e=>e,write:e=>String(e)},map:{read:e=>new Map(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e.entries()))},set:{read:e=>new Set(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e))},date:{read:e=>new Date(e),write:e=>e.toISOString()}},il="vueuse-storage";function dh(e,t,n,r={}){var o;const{flush:s="pre",deep:l=!0,listenToStorageChanges:i=!0,writeDefaults:a=!0,mergeDefaults:c=!1,shallow:u,window:f=wr,eventFilter:h,onError:g=m=>{console.error(m)}}=r,y=(u?Il:me)(t);if(!n)try{n=lh("getDefaultStorage",()=>{var m;return(m=wr)==null?void 0:m.localStorage})()}catch(m){g(m)}if(!n)return y;const w=Mn(t),C=ih(w),v=(o=r.serializer)!=null?o:fh[C],{pause:b,resume:P}=Xd(y,()=>S(y.value),{flush:s,deep:l,eventFilter:h});return f&&i&&(rl(f,"storage",$),rl(f,il,Z)),$(),y;function S(m){try{if(m==null)n.removeItem(e);else{const z=v.write(m),M=n.getItem(e);M!==z&&(n.setItem(e,z),f&&f.dispatchEvent(new CustomEvent(il,{detail:{key:e,oldValue:M,newValue:z,storageArea:n}})))}}catch(z){g(z)}}function U(m){const z=m?m.newValue:n.getItem(e);if(z==null)return a&&w!==null&&n.setItem(e,v.write(w)),w;if(!m&&c){const M=v.read(z);return typeof c=="function"?c(M,w):C==="object"&&!Array.isArray(M)?ll(ll({},w),M):M}else return typeof z!="string"?z:v.read(z)}function Z(m){$(m.detail)}function $(m){if(!(m&&m.storageArea!==n)){if(m&&m.key==null){y.value=w;return}if(!(m&&m.key!==e)){b();try{y.value=U(m)}catch(z){g(z)}finally{m?Sr(P):P()}}}}}function hh(e){return rh("(prefers-color-scheme: dark)",e)}const ph=()=>$i(),ze=()=>Sd(),Bi=Symbol(""),Vo=()=>{const e=Pe(Bi);if(!e)throw new Error("useDarkMode() is called without provider.");return e},mh=()=>{const e=ze(),t=hh(),n=dh("vuepress-color-scheme",e.value.colorMode),r=F({get(){return e.value.colorModeSwitch?n.value==="auto"?t.value:n.value==="dark":e.value.colorMode==="dark"},set(o){o===t.value?n.value="auto":n.value=o?"dark":"light"}});Wt(Bi,r),gh(r)},gh=e=>{const t=(n=e.value)=>{const r=window==null?void 0:window.document.querySelector("html");r==null||r.classList.toggle("dark",n)};Je(()=>{st(e,t,{immediate:!0})}),Rr(()=>t())},zi=(...e)=>{const n=Jt().resolve(...e),r=n.matched[n.matched.length-1];if(!(r!=null&&r.redirect))return n;const{redirect:o}=r,s=oe(o)?o(n):o,l=ge(s)?{path:s}:s;return zi({hash:n.hash,query:n.query,params:n.params,...l})},Uo=e=>{const t=zi(encodeURI(e));return{text:t.meta.title||e,link:t.name==="404"?e:t.fullPath}};let Qr=null,wn=null;const vh={wait:()=>Qr,pending:()=>{Qr=new Promise(e=>wn=e)},resolve:()=>{wn==null||wn(),Qr=null,wn=null}},Vi=()=>vh,Ui=Symbol("sidebarItems"),qo=()=>{const e=Pe(Ui);if(!e)throw new Error("useSidebarItems() is called without provider.");return e},_h=()=>{const e=ze(),t=gt(),n=F(()=>bh(t.value,e.value));Wt(Ui,n)},bh=(e,t)=>{const n=e.sidebar??t.sidebar??"auto",r=e.sidebarDepth??t.sidebarDepth??2;return e.home||n===!1?[]:n==="auto"?Eh(r):Q(n)?qi(n,r):No(n)?wh(n,r):[]},yh=(e,t)=>({text:e.title,link:e.link,children:Wo(e.children,t)}),Wo=(e,t)=>t>0?e.map(n=>yh(n,t-1)):[],Eh=e=>{const t=Gt();return[{text:t.value.title,children:Wo(t.value.headers,e)}]},qi=(e,t)=>{const n=Qt(),r=Gt(),o=s=>{var i;let l;if(ge(s)?l=Uo(s):l=s,l.children)return{...l,children:l.children.map(a=>o(a))};if(l.link===n.path){const a=((i=r.value.headers[0])==null?void 0:i.level)===1?r.value.headers[0].children:r.value.headers;return{...l,children:Wo(a,t)}}return l};return e.map(s=>o(s))},wh=(e,t)=>{const n=Qt(),r=di(e,n.path),o=e[r]??[];return qi(o,t)},kh="719px",xh={mobile:kh};var Hn;(function(e){e.MOBILE="mobile"})(Hn||(Hn={}));var hl;const Lh={[Hn.MOBILE]:Number.parseInt((hl=xh.mobile)==null?void 0:hl.replace("px",""),10)},Wi=(e,t)=>{const n=Lh[e];Number.isInteger(n)&&Je(()=>{t(n),window.addEventListener("resize",()=>t(n),!1),window.addEventListener("orientationchange",()=>t(n),!1)})},Ch={},Th={class:"theme-default-content"};function Sh(e,t){const n=bt("Content");return j(),X("div",Th,[te(n)])}const Ph=Le(Ch,[["render",Sh],["__file","HomeContent.vue"]]),Ah={key:0,class:"features"},Oh=ue({__name:"HomeFeatures",setup(e){const t=gt(),n=F(()=>Q(t.value.features)?t.value.features:[]);return(r,o)=>n.value.length?(j(),X("div",Ah,[(j(!0),X(we,null,It(n.value,s=>(j(),X("div",{key:s.title,class:"feature"},[he("h2",null,Re(s.title),1),he("p",null,Re(s.details),1)]))),128))])):Ce("v-if",!0)}}),Rh=Le(Oh,[["__file","HomeFeatures.vue"]]),Ih=["innerHTML"],$h=["textContent"],Nh=ue({__name:"HomeFooter",setup(e){const t=gt(),n=F(()=>t.value.footer),r=F(()=>t.value.footerHtml);return(o,s)=>n.value?(j(),X(we,{key:0},[Ce(" eslint-disable-next-line vue/no-v-html "),r.value?(j(),X("div",{key:0,class:"footer",innerHTML:n.value},null,8,Ih)):(j(),X("div",{key:1,class:"footer",textContent:Re(n.value)},null,8,$h))],64)):Ce("v-if",!0)}}),Dh=Le(Nh,[["__file","HomeFooter.vue"]]),Mh=["href","rel","target","aria-label"],Hh=ue({inheritAttrs:!1}),Fh=ue({...Hh,__name:"AutoLink",props:{item:{type:Object,required:!0}},setup(e){const t=e,n=Qt(),r=bi(),{item:o}=Tr(t),s=F(()=>Wn(o.value.link)),l=F(()=>Fu(o.value.link)||ju(o.value.link)),i=F(()=>{if(!l.value){if(o.value.target)return o.value.target;if(s.value)return"_blank"}}),a=F(()=>i.value==="_blank"),c=F(()=>!s.value&&!l.value&&!a.value),u=F(()=>{if(!l.value){if(o.value.rel)return o.value.rel;if(a.value)return"noopener noreferrer"}}),f=F(()=>o.value.ariaLabel||o.value.text),h=F(()=>{const w=Object.keys(r.value.locales);return w.length?!w.some(C=>C===o.value.link):o.value.link!=="/"}),g=F(()=>h.value?n.path.startsWith(o.value.link):!1),y=F(()=>c.value?o.value.activeMatch?new RegExp(o.value.activeMatch).test(n.path):g.value:!1);return(w,C)=>{const v=bt("RouterLink"),b=bt("AutoLinkExternalIcon");return c.value?(j(),Se(v,co({key:0,class:{"router-link-active":y.value},to:ee(o).link,"aria-label":f.value},w.$attrs),{default:De(()=>[Ee(w.$slots,"before"),Nt(" "+Re(ee(o).text)+" ",1),Ee(w.$slots,"after")]),_:3},16,["class","to","aria-label"])):(j(),X("a",co({key:1,class:"external-link",href:ee(o).link,rel:u.value,target:i.value,"aria-label":f.value},w.$attrs),[Ee(w.$slots,"before"),Nt(" "+Re(ee(o).text)+" ",1),a.value?(j(),Se(b,{key:0})):Ce("v-if",!0),Ee(w.$slots,"after")],16,Mh))}}}),vt=Le(Fh,[["__file","AutoLink.vue"]]),jh={class:"hero"},Bh={key:0,id:"main-title"},zh={key:1,class:"description"},Vh={key:2,class:"actions"},Uh=ue({__name:"HomeHero",setup(e){const t=gt(),n=Mo(),r=Vo(),o=F(()=>r.value&&t.value.heroImageDark!==void 0?t.value.heroImageDark:t.value.heroImage),s=F(()=>t.value.heroAlt||i.value||"hero"),l=F(()=>t.value.heroHeight||280),i=F(()=>t.value.heroText===null?null:t.value.heroText||n.value.title||"Hello"),a=F(()=>t.value.tagline===null?null:t.value.tagline||n.value.description||"Welcome to your VuePress site"),c=F(()=>Q(t.value.actions)?t.value.actions.map(({text:f,link:h,type:g="primary"})=>({text:f,link:h,type:g})):[]),u=()=>{if(!o.value)return null;const f=ae("img",{src:Fo(o.value),alt:s.value,height:l.value});return t.value.heroImageDark===void 0?f:ae(Ho,()=>f)};return(f,h)=>(j(),X("header",jh,[te(u),i.value?(j(),X("h1",Bh,Re(i.value),1)):Ce("v-if",!0),a.value?(j(),X("p",zh,Re(a.value),1)):Ce("v-if",!0),c.value.length?(j(),X("p",Vh,[(j(!0),X(we,null,It(c.value,g=>(j(),Se(vt,{key:g.text,class:qe(["action-button",[g.type]]),item:g},null,8,["class","item"]))),128))])):Ce("v-if",!0)]))}}),qh=Le(Uh,[["__file","HomeHero.vue"]]),Wh={class:"home"},Kh=ue({__name:"Home",setup(e){return(t,n)=>(j(),X("main",Wh,[te(qh),te(Rh),te(Ph),te(Dh)]))}}),Gh=Le(Kh,[["__file","Home.vue"]]),Yh=ue({__name:"NavbarBrand",setup(e){const t=Kn(),n=Mo(),r=ze(),o=Vo(),s=F(()=>r.value.home||t.value),l=F(()=>n.value.title),i=F(()=>o.value&&r.value.logoDark!==void 0?r.value.logoDark:r.value.logo),a=()=>{if(!i.value)return null;const c=ae("img",{class:"logo",src:Fo(i.value),alt:l.value});return r.value.logoDark===void 0?c:ae(Ho,()=>c)};return(c,u)=>{const f=bt("RouterLink");return j(),Se(f,{to:s.value},{default:De(()=>[te(a),l.value?(j(),X("span",{key:0,class:qe(["site-name",{"can-hide":i.value}])},Re(l.value),3)):Ce("v-if",!0)]),_:1},8,["to"])}}}),Jh=Le(Yh,[["__file","NavbarBrand.vue"]]),Qh=ue({__name:"DropdownTransition",setup(e){const t=r=>{r.style.height=r.scrollHeight+"px"},n=r=>{r.style.height=""};return(r,o)=>(j(),Se(qn,{name:"dropdown",onEnter:t,onAfterEnter:n,onBeforeLeave:t},{default:De(()=>[Ee(r.$slots,"default")]),_:3}))}}),Ki=Le(Qh,[["__file","DropdownTransition.vue"]]),Zh=["aria-label"],Xh={class:"title"},ep=he("span",{class:"arrow down"},null,-1),tp=["aria-label"],np={class:"title"},rp={class:"navbar-dropdown"},op={class:"navbar-dropdown-subtitle"},sp={key:1},lp={class:"navbar-dropdown-subitem-wrapper"},ip=ue({__name:"NavbarDropdown",props:{item:{type:Object,required:!0}},setup(e){const t=e,{item:n}=Tr(t),r=F(()=>n.value.ariaLabel||n.value.text),o=me(!1),s=Qt();st(()=>s.path,()=>{o.value=!1});const l=a=>{a.detail===0?o.value=!o.value:o.value=!1},i=(a,c)=>c[c.length-1]===a;return(a,c)=>(j(),X("div",{class:qe(["navbar-dropdown-wrapper",{open:o.value}])},[he("button",{class:"navbar-dropdown-title",type:"button","aria-label":r.value,onClick:l},[he("span",Xh,Re(ee(n).text),1),ep],8,Zh),he("button",{class:"navbar-dropdown-title-mobile",type:"button","aria-label":r.value,onClick:c[0]||(c[0]=u=>o.value=!o.value)},[he("span",np,Re(ee(n).text),1),he("span",{class:qe(["arrow",o.value?"down":"right"])},null,2)],8,tp),te(Ki,null,{default:De(()=>[pr(he("ul",rp,[(j(!0),X(we,null,It(ee(n).children,u=>(j(),X("li",{key:u.text,class:"navbar-dropdown-item"},[u.children?(j(),X(we,{key:0},[he("h4",op,[u.link?(j(),Se(vt,{key:0,item:u,onFocusout:f=>i(u,ee(n).children)&&u.children.length===0&&(o.value=!1)},null,8,["item","onFocusout"])):(j(),X("span",sp,Re(u.text),1))]),he("ul",lp,[(j(!0),X(we,null,It(u.children,f=>(j(),X("li",{key:f.link,class:"navbar-dropdown-subitem"},[te(vt,{item:f,onFocusout:h=>i(f,u.children)&&i(u,ee(n).children)&&(o.value=!1)},null,8,["item","onFocusout"])]))),128))])],64)):(j(),Se(vt,{key:1,item:u,onFocusout:f=>i(u,ee(n).children)&&(o.value=!1)},null,8,["item","onFocusout"]))]))),128))],512),[[br,o.value]])]),_:1})],2))}}),ap=Le(ip,[["__file","NavbarDropdown.vue"]]),al=e=>decodeURI(e).replace(/#.*$/,"").replace(/(index)?\.(md|html)$/,""),cp=(e,t)=>{if(t.hash===e)return!0;const n=al(t.path),r=al(e);return n===r},Gi=(e,t)=>e.link&&cp(e.link,t)?!0:e.children?e.children.some(n=>Gi(n,t)):!1,Yi=e=>!Wn(e)||/github\.com/.test(e)?"GitHub":/bitbucket\.org/.test(e)?"Bitbucket":/gitlab\.com/.test(e)?"GitLab":/gitee\.com/.test(e)?"Gitee":null,up={GitHub:":repo/edit/:branch/:path",GitLab:":repo/-/edit/:branch/:path",Gitee:":repo/edit/:branch/:path",Bitbucket:":repo/src/:branch/:path?mode=edit&spa=0&at=:branch&fileviewer=file-view-default"},fp=({docsRepo:e,editLinkPattern:t})=>{if(t)return t;const n=Yi(e);return n!==null?up[n]:null},dp=({docsRepo:e,docsBranch:t,docsDir:n,filePathRelative:r,editLinkPattern:o})=>{if(!r)return null;const s=fp({docsRepo:e,editLinkPattern:o});return s?s.replace(/:repo/,Wn(e)?e:`https://github.com/${e}`).replace(/:branch/,t).replace(/:path/,fi(`${ui(n)}/${r}`)):null},hp={key:0,class:"navbar-items"},pp=ue({__name:"NavbarItems",setup(e){const t=()=>{const u=Jt(),f=Kn(),h=bi(),g=Mo(),y=ph(),w=ze();return F(()=>{const C=Object.keys(h.value.locales);if(C.length<2)return[];const v=u.currentRoute.value.path,b=u.currentRoute.value.fullPath;return[{text:`${w.value.selectLanguageText}`,ariaLabel:`${w.value.selectLanguageAriaLabel??w.value.selectLanguageText}`,children:C.map(S=>{var M,G;const U=((M=h.value.locales)==null?void 0:M[S])??{},Z=((G=y.value.locales)==null?void 0:G[S])??{},$=`${U.lang}`,m=Z.selectLanguageName??$;let z;if($===g.value.lang)z=b;else{const L=v.replace(f.value,S);u.getRoutes().some(R=>R.path===L)?z=b.replace(v,L):z=Z.home??S}return{text:m,link:z}})}]})},n=()=>{const u=ze(),f=F(()=>u.value.repo),h=F(()=>f.value?Yi(f.value):null),g=F(()=>f.value&&!Wn(f.value)?`https://github.com/${f.value}`:f.value),y=F(()=>g.value?u.value.repoLabel?u.value.repoLabel:h.value===null?"Source":h.value:null);return F(()=>!g.value||!y.value?[]:[{text:y.value,link:g.value}])},r=u=>ge(u)?Uo(u):u.children?{...u,children:u.children.map(r)}:u,o=()=>{const u=ze();return F(()=>(u.value.navbar||[]).map(r))},s=me(!1),l=o(),i=t(),a=n(),c=F(()=>[...l.value,...i.value,...a.value]);return Wi(Hn.MOBILE,u=>{window.innerWidthc.value.length?(j(),X("nav",hp,[(j(!0),X(we,null,It(c.value,h=>(j(),X("div",{key:h.text,class:"navbar-item"},[h.children?(j(),Se(ap,{key:0,item:h,class:qe(s.value?"mobile":"")},null,8,["item","class"])):(j(),Se(vt,{key:1,item:h},null,8,["item"]))]))),128))])):Ce("v-if",!0)}}),Ji=Le(pp,[["__file","NavbarItems.vue"]]),mp=["title"],gp={class:"icon",focusable:"false",viewBox:"0 0 32 32"},vp=Gc('',9),_p=[vp],bp={class:"icon",focusable:"false",viewBox:"0 0 32 32"},yp=he("path",{d:"M13.502 5.414a15.075 15.075 0 0 0 11.594 18.194a11.113 11.113 0 0 1-7.975 3.39c-.138 0-.278.005-.418 0a11.094 11.094 0 0 1-3.2-21.584M14.98 3a1.002 1.002 0 0 0-.175.016a13.096 13.096 0 0 0 1.825 25.981c.164.006.328 0 .49 0a13.072 13.072 0 0 0 10.703-5.555a1.01 1.01 0 0 0-.783-1.565A13.08 13.08 0 0 1 15.89 4.38A1.015 1.015 0 0 0 14.98 3z",fill:"currentColor"},null,-1),Ep=[yp],wp=ue({__name:"ToggleColorModeButton",setup(e){const t=ze(),n=Vo(),r=()=>{n.value=!n.value};return(o,s)=>(j(),X("button",{class:"toggle-color-mode-button",title:ee(t).toggleColorMode,onClick:r},[pr((j(),X("svg",gp,_p,512)),[[br,!ee(n)]]),pr((j(),X("svg",bp,Ep,512)),[[br,ee(n)]])],8,mp))}}),kp=Le(wp,[["__file","ToggleColorModeButton.vue"]]),xp=["title"],Lp=he("div",{class:"icon","aria-hidden":"true"},[he("span"),he("span"),he("span")],-1),Cp=[Lp],Tp=ue({__name:"ToggleSidebarButton",emits:["toggle"],setup(e){const t=ze();return(n,r)=>(j(),X("div",{class:"toggle-sidebar-button",title:ee(t).toggleSidebar,"aria-expanded":"false",role:"button",tabindex:"0",onClick:r[0]||(r[0]=o=>n.$emit("toggle"))},Cp,8,xp))}}),Sp=Le(Tp,[["__file","ToggleSidebarButton.vue"]]),Pp=ue({__name:"Navbar",emits:["toggle-sidebar"],setup(e){const t=ze(),n=me(null),r=me(null),o=me(0),s=F(()=>o.value?{maxWidth:o.value+"px"}:{});Wi(Hn.MOBILE,i=>{var c;const a=l(n.value,"paddingLeft")+l(n.value,"paddingRight");window.innerWidth{const c=bt("NavbarSearch");return j(),X("header",{ref_key:"navbar",ref:n,class:"navbar"},[te(Sp,{onToggle:a[0]||(a[0]=u=>i.$emit("toggle-sidebar"))}),he("span",{ref_key:"navbarBrand",ref:r},[te(Jh)],512),he("div",{class:"navbar-items-wrapper",style:jn(s.value)},[Ee(i.$slots,"before"),te(Ji,{class:"can-hide"}),Ee(i.$slots,"after"),ee(t).colorModeSwitch?(j(),Se(kp,{key:0})):Ce("v-if",!0),te(c)],4)],512)}}}),Ap=Le(Pp,[["__file","Navbar.vue"]]),Op={class:"page-meta"},Rp={key:0,class:"meta-item edit-link"},Ip={key:1,class:"meta-item last-updated"},$p={class:"meta-item-label"},Np={class:"meta-item-info"},Dp={key:2,class:"meta-item contributors"},Mp={class:"meta-item-label"},Hp={class:"meta-item-info"},Fp=["title"],jp=ue({__name:"PageMeta",setup(e){const t=()=>{const a=ze(),c=Gt(),u=gt();return F(()=>{if(!(u.value.editLink??a.value.editLink??!0))return null;const{repo:h,docsRepo:g=h,docsBranch:y="main",docsDir:w="",editLinkText:C}=a.value;if(!g)return null;const v=dp({docsRepo:g,docsBranch:y,docsDir:w,filePathRelative:c.value.filePathRelative,editLinkPattern:u.value.editLinkPattern??a.value.editLinkPattern});return v?{text:C??"Edit this page",link:v}:null})},n=()=>{const a=ze(),c=Gt(),u=gt();return F(()=>{var g,y;return!(u.value.lastUpdated??a.value.lastUpdated??!0)||!((g=c.value.git)!=null&&g.updatedTime)?null:new Date((y=c.value.git)==null?void 0:y.updatedTime).toLocaleString()})},r=()=>{const a=ze(),c=Gt(),u=gt();return F(()=>{var h;return u.value.contributors??a.value.contributors??!0?((h=c.value.git)==null?void 0:h.contributors)??null:null})},o=ze(),s=t(),l=n(),i=r();return(a,c)=>{const u=bt("ClientOnly");return j(),X("footer",Op,[ee(s)?(j(),X("div",Rp,[te(vt,{class:"meta-item-label",item:ee(s)},null,8,["item"])])):Ce("v-if",!0),ee(l)?(j(),X("div",Ip,[he("span",$p,Re(ee(o).lastUpdatedText)+": ",1),te(u,null,{default:De(()=>[he("span",Np,Re(ee(l)),1)]),_:1})])):Ce("v-if",!0),ee(i)&&ee(i).length?(j(),X("div",Dp,[he("span",Mp,Re(ee(o).contributorsText)+": ",1),he("span",Hp,[(j(!0),X(we,null,It(ee(i),(f,h)=>(j(),X(we,{key:h},[he("span",{class:"contributor",title:`email: ${f.email}`},Re(f.name),9,Fp),h!==ee(i).length-1?(j(),X(we,{key:0},[Nt(", ")],64)):Ce("v-if",!0)],64))),128))])])):Ce("v-if",!0)])}}}),Bp=Le(jp,[["__file","PageMeta.vue"]]),zp={key:0,class:"page-nav"},Vp={class:"inner"},Up={key:0,class:"prev"},qp={key:1,class:"next"},Wp=ue({__name:"PageNav",setup(e){const t=a=>a===!1?null:ge(a)?Uo(a):No(a)?a:!1,n=(a,c,u)=>{const f=a.findIndex(h=>h.link===c);if(f!==-1){const h=a[f+u];return h!=null&&h.link?h:null}for(const h of a)if(h.children){const g=n(h.children,c,u);if(g)return g}return null},r=gt(),o=qo(),s=Qt(),l=F(()=>{const a=t(r.value.prev);return a!==!1?a:n(o.value,s.path,-1)}),i=F(()=>{const a=t(r.value.next);return a!==!1?a:n(o.value,s.path,1)});return(a,c)=>l.value||i.value?(j(),X("nav",zp,[he("p",Vp,[l.value?(j(),X("span",Up,[te(vt,{item:l.value},null,8,["item"])])):Ce("v-if",!0),i.value?(j(),X("span",qp,[te(vt,{item:i.value},null,8,["item"])])):Ce("v-if",!0)])])):Ce("v-if",!0)}}),Kp=Le(Wp,[["__file","PageNav.vue"]]),Gp={class:"page"},Yp={class:"theme-default-content"},Jp=ue({__name:"Page",setup(e){return(t,n)=>{const r=bt("Content");return j(),X("main",Gp,[Ee(t.$slots,"top"),he("div",Yp,[Ee(t.$slots,"content-top"),te(r),Ee(t.$slots,"content-bottom")]),te(Bp),te(Kp),Ee(t.$slots,"bottom")])}}}),Qp=Le(Jp,[["__file","Page.vue"]]),Zp=["onKeydown"],Xp={class:"sidebar-item-children"},em=ue({__name:"SidebarItem",props:{item:{type:Object,required:!0},depth:{type:Number,required:!1,default:0}},setup(e){const t=e,{item:n,depth:r}=Tr(t),o=Qt(),s=Jt(),l=F(()=>Gi(n.value,o)),i=F(()=>({"sidebar-item":!0,"sidebar-heading":r.value===0,active:l.value,collapsible:n.value.collapsible})),a=F(()=>n.value.collapsible?l.value:!0),[c,u]=zd(a.value),f=g=>{n.value.collapsible&&(g.preventDefault(),u())},h=s.afterEach(g=>{Sr(()=>{c.value=a.value})});return Un(()=>{h()}),(g,y)=>{var C;const w=bt("SidebarItem",!0);return j(),X("li",null,[ee(n).link?(j(),Se(vt,{key:0,class:qe(i.value),item:ee(n)},null,8,["class","item"])):(j(),X("p",{key:1,tabindex:"0",class:qe(i.value),onClick:f,onKeydown:Au(f,["enter"])},[Nt(Re(ee(n).text)+" ",1),ee(n).collapsible?(j(),X("span",{key:0,class:qe(["arrow",ee(c)?"down":"right"])},null,2)):Ce("v-if",!0)],42,Zp)),(C=ee(n).children)!=null&&C.length?(j(),Se(Ki,{key:2},{default:De(()=>[pr(he("ul",Xp,[(j(!0),X(we,null,It(ee(n).children,v=>(j(),Se(w,{key:`${ee(r)}${v.text}${v.link}`,item:v,depth:ee(r)+1},null,8,["item","depth"]))),128))],512),[[br,ee(c)]])]),_:1})):Ce("v-if",!0)])}}}),tm=Le(em,[["__file","SidebarItem.vue"]]),nm={key:0,class:"sidebar-items"},rm=ue({__name:"SidebarItems",setup(e){const t=Qt(),n=qo();return Je(()=>{st(()=>t.hash,r=>{const o=document.querySelector(".sidebar");if(!o)return;const s=document.querySelector(`.sidebar a.sidebar-item[href="${t.path}${r}"]`);if(!s)return;const{top:l,height:i}=o.getBoundingClientRect(),{top:a,height:c}=s.getBoundingClientRect();al+i&&s.scrollIntoView(!1)})}),(r,o)=>ee(n).length?(j(),X("ul",nm,[(j(!0),X(we,null,It(ee(n),s=>(j(),Se(tm,{key:`${s.text}${s.link}`,item:s},null,8,["item"]))),128))])):Ce("v-if",!0)}}),om=Le(rm,[["__file","SidebarItems.vue"]]),sm={class:"sidebar"},lm=ue({__name:"Sidebar",setup(e){return(t,n)=>(j(),X("aside",sm,[te(Ji),Ee(t.$slots,"top"),te(om),Ee(t.$slots,"bottom")]))}}),im=Le(lm,[["__file","Sidebar.vue"]]),am=ue({__name:"Layout",setup(e){const t=Gt(),n=gt(),r=ze(),o=F(()=>n.value.navbar!==!1&&r.value.navbar!==!1),s=qo(),l=me(!1),i=C=>{l.value=typeof C=="boolean"?C:!l.value},a={x:0,y:0},c=C=>{a.x=C.changedTouches[0].clientX,a.y=C.changedTouches[0].clientY},u=C=>{const v=C.changedTouches[0].clientX-a.x,b=C.changedTouches[0].clientY-a.y;Math.abs(v)>Math.abs(b)&&Math.abs(v)>40&&(v>0&&a.x<=80?i(!0):i(!1))},f=F(()=>[{"no-navbar":!o.value,"no-sidebar":!s.value.length,"sidebar-open":l.value},n.value.pageClass]);let h;Je(()=>{h=Jt().afterEach(()=>{i(!1)})}),Rr(()=>{h()});const g=Vi(),y=g.resolve,w=g.pending;return(C,v)=>(j(),X("div",{class:qe(["theme-container",f.value]),onTouchstart:c,onTouchend:u},[Ee(C.$slots,"navbar",{},()=>[o.value?(j(),Se(Ap,{key:0,onToggleSidebar:i},{before:De(()=>[Ee(C.$slots,"navbar-before")]),after:De(()=>[Ee(C.$slots,"navbar-after")]),_:3})):Ce("v-if",!0)]),he("div",{class:"sidebar-mask",onClick:v[0]||(v[0]=b=>i(!1))}),Ee(C.$slots,"sidebar",{},()=>[te(im,null,{top:De(()=>[Ee(C.$slots,"sidebar-top")]),bottom:De(()=>[Ee(C.$slots,"sidebar-bottom")]),_:3})]),Ee(C.$slots,"page",{},()=>[ee(n).home?(j(),Se(Gh,{key:0})):(j(),Se(qn,{key:1,name:"fade-slide-y",mode:"out-in",onBeforeEnter:ee(y),onBeforeLeave:ee(w)},{default:De(()=>[(j(),Se(Qp,{key:ee(t).path},{top:De(()=>[Ee(C.$slots,"page-top")]),"content-top":De(()=>[Ee(C.$slots,"page-content-top")]),"content-bottom":De(()=>[Ee(C.$slots,"page-content-bottom")]),bottom:De(()=>[Ee(C.$slots,"page-bottom")]),_:3}))]),_:3},8,["onBeforeEnter","onBeforeLeave"]))])],34))}}),cm=Le(am,[["__file","Layout.vue"]]),um={class:"theme-container"},fm={class:"page"},dm={class:"theme-default-content"},hm=he("h1",null,"404",-1),pm=ue({__name:"NotFound",setup(e){const t=Kn(),n=ze(),r=n.value.notFound??["Not Found"],o=()=>r[Math.floor(Math.random()*r.length)],s=n.value.home??t.value,l=n.value.backToHome??"Back to home";return(i,a)=>{const c=bt("RouterLink");return j(),X("div",um,[he("main",fm,[he("div",dm,[hm,he("blockquote",null,Re(o()),1),te(c,{to:ee(s)},{default:De(()=>[Nt(Re(ee(l)),1)]),_:1},8,["to"])])])])}}}),mm=Le(pm,[["__file","NotFound.vue"]]);const gm=Dt({enhance({app:e,router:t}){e.component("Badge",Rd),e.component("CodeGroup",Id),e.component("CodeGroupItem",Md),e.component("AutoLinkExternalIcon",()=>{const r=e.component("ExternalLinkIcon");return r?ae(r):null}),e.component("NavbarSearch",()=>{const r=e.component("Docsearch")||e.component("SearchBox");return r?ae(r):null});const n=t.options.scrollBehavior;t.options.scrollBehavior=async(...r)=>(await Vi().wait(),n(...r))},setup(){mh(),_h()},layouts:{Layout:cm,NotFound:mm}}),vm=e=>e instanceof Element?document.activeElement===e&&(["TEXTAREA","SELECT","INPUT"].includes(e.tagName)||e.hasAttribute("contenteditable")):!1,_m=(e,t)=>t.some(n=>{if(ge(n))return n===e.key;const{key:r,ctrl:o=!1,shift:s=!1,alt:l=!1}=n;return r===e.key&&o===e.ctrlKey&&s===e.shiftKey&&l===e.altKey}),bm=/[^\x00-\x7F]/,ym=e=>e.split(/\s+/g).map(t=>t.trim()).filter(t=>!!t),cl=e=>e.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"),ul=(e,t)=>{const n=t.join(" "),r=ym(e);if(bm.test(e))return r.some(l=>n.toLowerCase().indexOf(l)>-1);const o=e.endsWith(" ");return new RegExp(r.map((l,i)=>r.length===i+1&&!o?`(?=.*\\b${cl(l)})`:`(?=.*\\b${cl(l)}\\b)`).join("")+".+","gi").test(n)},Em=({input:e,hotKeys:t})=>{if(t.value.length===0)return;const n=r=>{e.value&&_m(r,t.value)&&!vm(r.target)&&(r.preventDefault(),e.value.focus())};Je(()=>{document.addEventListener("keydown",n)}),Un(()=>{document.removeEventListener("keydown",n)})},wm=[{title:"",headers:[{level:2,title:"☘️Introduction",slug:"☘️introduction",link:"#☘️introduction",children:[]},{level:2,title:"📖使用说明",slug:"📖使用说明",link:"#📖使用说明",children:[{level:3,title:"1. 安装",slug:"_1-安装",link:"#_1-安装",children:[]},{level:3,title:"2. 开发",slug:"_2-开发",link:"#_2-开发",children:[]}]},{level:2,title:"部署",slug:"部署",link:"#部署",children:[]}],path:"/develop.html",pathLocale:"/",extraFields:[]},{title:"介绍",headers:[{level:2,title:"新书推荐",slug:"新书推荐",link:"#新书推荐",children:[]},{level:2,title:"seldom框架",slug:"seldom框架",link:"#seldom框架",children:[{level:3,title:"特点",slug:"特点",link:"#特点",children:[]},{level:3,title:"设计理念",slug:"设计理念",link:"#设计理念",children:[]},{level:3,title:"发展历史",slug:"发展历史",link:"#发展历史",children:[]},{level:3,title:"seldom vs pytest",slug:"seldom-vs-pytest",link:"#seldom-vs-pytest",children:[]}]}],path:"/introduce.html",pathLocale:"/",extraFields:[]},{title:"",headers:[],path:"/",pathLocale:"/",extraFields:[]},{title:"appium API",headers:[{level:2,title:"appium 定位",slug:"appium-定位",link:"#appium-定位",children:[]},{level:2,title:"appium lab",slug:"appium-lab",link:"#appium-lab",children:[]},{level:2,title:"appium driver",slug:"appium-driver",link:"#appium-driver",children:[]}],path:"/app-testing/appium_lab.html",pathLocale:"/",extraFields:[]},{title:"appium 扩展",headers:[{level:2,title:"appium images-plugin",slug:"appium-images-plugin",link:"#appium-images-plugin",children:[]},{level:2,title:"Appium OCR plugin",slug:"appium-ocr-plugin",link:"#appium-ocr-plugin",children:[]}],path:"/app-testing/extensions.html",pathLocale:"/",extraFields:[]},{title:"Page Object",headers:[],path:"/app-testing/page_object.html",pathLocale:"/",extraFields:[]},{title:"app 测试",headers:[{level:2,title:"环境安装",slug:"环境安装",link:"#环境安装",children:[]},{level:2,title:"编写测试",slug:"编写测试",link:"#编写测试",children:[]}],path:"/app-testing/start.html",pathLocale:"/",extraFields:[]},{title:"支持Excel测试用例",headers:[{level:3,title:"编写Excel用例",slug:"编写excel用例",link:"#编写excel用例",children:[]},{level:3,title:"运行测试用例",slug:"运行测试用例",link:"#运行测试用例",children:[]}],path:"/api-testing/api_case.html",pathLocale:"/",extraFields:[]},{title:"API Object",headers:[],path:"/api-testing/api_object.html",pathLocale:"/",extraFields:[]},{title:"更强大的断言",headers:[{level:3,title:"assertJSON",slug:"assertjson",link:"#assertjson",children:[]},{level:3,title:"assertPath",slug:"assertpath",link:"#assertpath",children:[]},{level:3,title:"assertSchema",slug:"assertschema",link:"#assertschema",children:[]}],path:"/api-testing/assert.html",pathLocale:"/",extraFields:[]},{title:"更多功能",headers:[{level:3,title:"har to case",slug:"har-to-case",link:"#har-to-case",children:[]},{level:3,title:"swagger to case",slug:"swagger-to-case",link:"#swagger-to-case",children:[]},{level:3,title:"请求转 cURL",slug:"请求转-curl",link:"#请求转-curl",children:[]},{level:3,title:"接口数据依赖",slug:"接口数据依赖",link:"#接口数据依赖",children:[]},{level:3,title:"Session使用",slug:"session使用",link:"#session使用",children:[]},{level:3,title:"提取接口返回数据",slug:"提取接口返回数据",link:"#提取接口返回数据",children:[]},{level:3,title:"genson",slug:"genson",link:"#genson",children:[]},{level:3,title:"mock URL",slug:"mock-url",link:"#mock-url",children:[]},{level:3,title:"配置proxies代理",slug:"配置proxies代理",link:"#配置proxies代理",children:[]},{level:3,title:"@retry装饰器",slug:"retry装饰器",link:"#retry装饰器",children:[]}],path:"/api-testing/more.html",pathLocale:"/",extraFields:[]},{title:"开始使用",headers:[{level:3,title:"前言",slug:"前言",link:"#前言",children:[]},{level:3,title:"Seldom VS Request+unittest",slug:"seldom-vs-request-unittest",link:"#seldom-vs-request-unittest",children:[]},{level:3,title:"运行测试",slug:"运行测试",link:"#运行测试",children:[]}],path:"/api-testing/start.html",pathLocale:"/",extraFields:[]},{title:"WebSocket",headers:[{level:3,title:"WebSocket 生命周期",slug:"websocket-生命周期",link:"#websocket-生命周期",children:[]},{level:3,title:"seldom测试WebSocket",slug:"seldom测试websocket",link:"#seldom测试websocket",children:[]}],path:"/api-testing/webscocket.html",pathLocale:"/",extraFields:[]},{title:"高级用法",headers:[{level:3,title:"fixture",slug:"fixture",link:"#fixture",children:[]},{level:3,title:"跳过测试",slug:"跳过测试",link:"#跳过测试",children:[]},{level:3,title:"重复执行",slug:"重复执行",link:"#重复执行",children:[]},{level:3,title:"随机测试数据",slug:"随机测试数据",link:"#随机测试数据",children:[]},{level:3,title:"用例的依赖",slug:"用例的依赖",link:"#用例的依赖",children:[]},{level:3,title:"用例分类标签",slug:"用例分类标签",link:"#用例分类标签",children:[]},{level:3,title:"发送邮件",slug:"发送邮件",link:"#发送邮件",children:[]},{level:3,title:"发送钉钉",slug:"发送钉钉",link:"#发送钉钉",children:[]},{level:3,title:"seldom日志",slug:"seldom日志",link:"#seldom日志",children:[]},{level:3,title:"缓存 cache",slug:"缓存-cache",link:"#缓存-cache",children:[]}],path:"/getting-started/advanced.html",pathLocale:"/",extraFields:[]},{title:"创建项目",headers:[{level:3,title:"自动生成项目",slug:"自动生成项目",link:"#自动生成项目",children:[]},{level:3,title:"创建测试用例",slug:"创建测试用例",link:"#创建测试用例",children:[]}],path:"/getting-started/create_project.html",pathLocale:"/",extraFields:[]},{title:"数据驱动",headers:[{level:3,title:"@class_data() 方法",slug:"class-data-方法",link:"#class-data-方法",children:[]},{level:3,title:"@data()方法",slug:"data-方法",link:"#data-方法",children:[]},{level:3,title:"@file_data() 方法",slug:"file-data-方法",link:"#file-data-方法",children:[]},{level:3,title:"@api_data()方法",slug:"api-data-方法",link:"#api-data-方法",children:[]},{level:3,title:"使用函数构造数据",slug:"使用函数构造数据",link:"#使用函数构造数据",children:[]},{level:3,title:"支持第三方 ddt 库",slug:"支持第三方-ddt-库",link:"#支持第三方-ddt-库",children:[]}],path:"/getting-started/data_driver.html",pathLocale:"/",extraFields:[]},{title:"方法的依赖",headers:[{level:3,title:"类内部方法调用",slug:"类内部方法调用",link:"#类内部方法调用",children:[]},{level:3,title:"外部类方法依赖",slug:"外部类方法依赖",link:"#外部类方法依赖",children:[]},{level:3,title:"多重方法依赖",slug:"多重方法依赖",link:"#多重方法依赖",children:[]},{level:3,title:"参数化使用",slug:"参数化使用",link:"#参数化使用",children:[]}],path:"/getting-started/dependent_func.html",pathLocale:"/",extraFields:[]},{title:"Installation",headers:[],path:"/getting-started/installation.html",pathLocale:"/",extraFields:[]},{title:"快速开始",headers:[{level:3,title:"基本规范",slug:"基本规范",link:"#基本规范",children:[]},{level:3,title:"main() 方法",slug:"main-方法",link:"#main-方法",children:[]},{level:3,title:"confrun.py 配置文件",slug:"confrun-py-配置文件",link:"#confrun-py-配置文件",children:[]},{level:3,title:"运行测试",slug:"运行测试",link:"#运行测试",children:[]},{level:3,title:"失败重跑",slug:"失败重跑",link:"#失败重跑",children:[]},{level:3,title:"测试报告",slug:"测试报告",link:"#测试报告",children:[]},{level:3,title:"多线程运行",slug:"多线程运行",link:"#多线程运行",children:[]}],path:"/getting-started/quick_start.html",pathLocale:"/",extraFields:[]},{title:"seldom CLI",headers:[{level:2,title:"seldom 帮助",slug:"seldom-帮助",link:"#seldom-帮助",children:[]},{level:2,title:"seldom 使用",slug:"seldom-使用",link:"#seldom-使用",children:[{level:3,title:"创建项目",slug:"创建项目",link:"#创建项目",children:[]},{level:3,title:"生成接口自动化用例",slug:"生成接口自动化用例",link:"#生成接口自动化用例",children:[]},{level:3,title:"运行测试目录&文件",slug:"运行测试目录-文件",link:"#运行测试目录-文件",children:[]},{level:3,title:"运行文件&类&方法",slug:"运行文件-类-方法",link:"#运行文件-类-方法",children:[]},{level:3,title:"调试模式",slug:"调试模式",link:"#调试模式",children:[]},{level:3,title:"运行浏览器",slug:"运行浏览器",link:"#运行浏览器",children:[]},{level:3,title:"运行URL",slug:"运行url",link:"#运行url",children:[]},{level:3,title:"测试报告",slug:"测试报告",link:"#测试报告",children:[]},{level:3,title:"失败/错误重跑次数",slug:"失败-错误重跑次数",link:"#失败-错误重跑次数",children:[]},{level:3,title:"数据驱动运行环境",slug:"数据驱动运行环境",link:"#数据驱动运行环境",children:[]},{level:3,title:"收集测试用例",slug:"收集测试用例",link:"#收集测试用例",children:[]},{level:3,title:"运行收集测试用例",slug:"运行收集测试用例",link:"#运行收集测试用例",children:[]},{level:3,title:"清除所有缓存",slug:"清除所有缓存",link:"#清除所有缓存",children:[]},{level:3,title:"执行 API(excel文件)测试用例",slug:"执行-api-excel文件-测试用例",link:"#执行-api-excel文件-测试用例",children:[]}]}],path:"/getting-started/seldom_cli.html",pathLocale:"/",extraFields:[]},{title:"平台化支持",headers:[{level:3,title:"获取用例信息",slug:"获取用例信息",link:"#获取用例信息",children:[]},{level:3,title:"执行用例信息",slug:"执行用例信息",link:"#执行用例信息",children:[]},{level:3,title:"接入平台比读",slug:"接入平台比读",link:"#接入平台比读",children:[]}],path:"/platform/platform.html",pathLocale:"/",extraFields:[]},{title:"数据库操作",headers:[{level:3,title:"连接数据库",slug:"连接数据库",link:"#连接数据库",children:[]},{level:3,title:"操作方法",slug:"操作方法",link:"#操作方法",children:[]},{level:2,title:"MongoDB",slug:"mongodb",link:"#mongodb",children:[]}],path:"/more-ability/db_operation.html",pathLocale:"/",extraFields:[]},{title:"支持更多测试库",headers:[{level:3,title:"使用playwright",slug:"使用playwright",link:"#使用playwright",children:[]},{level:3,title:"使用uiautomator2",slug:"使用uiautomator2",link:"#使用uiautomator2",children:[]},{level:3,title:"使用pyAutoGUI",slug:"使用pyautogui",link:"#使用pyautogui",children:[]}],path:"/more-ability/test_library.html",pathLocale:"/",extraFields:[]},{title:"浏览器与驱动",headers:[{level:3,title:"管理浏览器驱动",slug:"管理浏览器驱动",link:"#管理浏览器驱动",children:[]},{level:3,title:"指定浏览器驱动",slug:"指定浏览器驱动",link:"#指定浏览器驱动",children:[]},{level:3,title:"指定不同的浏览器",slug:"指定不同的浏览器",link:"#指定不同的浏览器",children:[]},{level:3,title:"控制浏览器启动和关闭",slug:"控制浏览器启动和关闭",link:"#控制浏览器启动和关闭",children:[]}],path:"/web-testing/browser_driver.html",pathLocale:"/",extraFields:[]},{title:"链式调用",headers:[{level:3,title:"基本例子",slug:"基本例子",link:"#基本例子",children:[]},{level:3,title:"Steps 类",slug:"steps-类",link:"#steps-类",children:[]},{level:3,title:"控制浏览器启动和关闭",slug:"控制浏览器启动和关闭",link:"#控制浏览器启动和关闭",children:[]}],path:"/web-testing/chaining.html",pathLocale:"/",extraFields:[]},{title:"浏览器启动配置",headers:[{level:3,title:"使用headless模式",slug:"使用headless模式",link:"#使用headless模式",children:[]},{level:3,title:"Selenium Grid",slug:"selenium-grid",link:"#selenium-grid",children:[]},{level:3,title:"Mobile Web 模式",slug:"mobile-web-模式",link:"#mobile-web-模式",children:[]},{level:3,title:"浏览器忽略无效证书",slug:"浏览器忽略无效证书",link:"#浏览器忽略无效证书",children:[]},{level:3,title:"浏览器关闭沙盒模式",slug:"浏览器关闭沙盒模式",link:"#浏览器关闭沙盒模式",children:[]},{level:3,title:"开启实验性功能",slug:"开启实验性功能",link:"#开启实验性功能",children:[]},{level:3,title:"设置浏览器代理",slug:"设置浏览器代理",link:"#设置浏览器代理",children:[]},{level:3,title:"连接已打开浏览器",slug:"连接已打开浏览器",link:"#连接已打开浏览器",children:[]}],path:"/web-testing/other.html",pathLocale:"/",extraFields:[]},{title:"Page Object",headers:[],path:"/web-testing/page_object.html",pathLocale:"/",extraFields:[]},{title:"Seldom API",headers:[{level:3,title:"查找元素",slug:"查找元素",link:"#查找元素",children:[]},{level:3,title:"断言",slug:"断言",link:"#断言",children:[]},{level:3,title:"WebDriver API",slug:"webdriver-api",link:"#webdriver-api",children:[]},{level:3,title:"键盘操作",slug:"键盘操作",link:"#键盘操作",children:[]},{level:3,title:"测试electron应用",slug:"测试electron应用",link:"#测试electron应用",children:[]}],path:"/web-testing/seldom_api.html",pathLocale:"/",extraFields:[]},{title:"版本更新",headers:[{level:3,title:"seldom 3.x",slug:"seldom-3-x",link:"#seldom-3-x",children:[]},{level:3,title:"seldom 2.x",slug:"seldom-2-x",link:"#seldom-2-x",children:[]},{level:3,title:"seldom 1.x",slug:"seldom-1-x",link:"#seldom-1-x",children:[]},{level:3,title:"seldom 0.x",slug:"seldom-0-x",link:"#seldom-0-x",children:[]},{level:3,title:"pyse",slug:"pyse",link:"#pyse",children:[]}],path:"/version/CHANGES.html",pathLocale:"/",extraFields:[]},{title:"",headers:[],path:"/404.html",pathLocale:"/",extraFields:[]}],km=me(wm),xm=()=>km,Lm=({searchIndex:e,routeLocale:t,query:n,maxSuggestions:r})=>{const o=F(()=>e.value.filter(s=>s.pathLocale===t.value));return F(()=>{const s=n.value.trim().toLowerCase();if(!s)return[];const l=[],i=(a,c)=>{ul(s,[c.title])&&l.push({link:`${a.path}#${c.slug}`,title:a.title,header:c.title});for(const u of c.children){if(l.length>=r.value)return;i(a,u)}};for(const a of o.value){if(l.length>=r.value)break;if(ul(s,[a.title,...a.extraFields])){l.push({link:a.path,title:a.title});continue}for(const c of a.headers){if(l.length>=r.value)break;i(a,c)}}return l})},Cm=e=>{const t=me(0);return{focusIndex:t,focusNext:()=>{t.value{t.value>0?t.value-=1:t.value=e.value.length-1}}},Tm=ue({name:"SearchBox",props:{locales:{type:Object,required:!1,default:()=>({})},hotKeys:{type:Array,required:!1,default:()=>[]},maxSuggestions:{type:Number,required:!1,default:5}},setup(e){const{locales:t,hotKeys:n,maxSuggestions:r}=Tr(e),o=Jt(),s=Kn(),l=xm(),i=me(null),a=me(!1),c=me(""),u=F(()=>t.value[s.value]??{}),f=Lm({searchIndex:l,routeLocale:s,query:c,maxSuggestions:r}),{focusIndex:h,focusNext:g,focusPrev:y}=Cm(f);Em({input:i,hotKeys:n});const w=F(()=>a.value&&!!f.value.length),C=()=>{w.value&&y()},v=()=>{w.value&&g()},b=P=>{if(!w.value)return;const S=f.value[P];S&&o.push(S.link).then(()=>{c.value="",h.value=0})};return()=>ae("form",{class:"search-box",role:"search"},[ae("input",{ref:i,type:"search",placeholder:u.value.placeholder,autocomplete:"off",spellcheck:!1,value:c.value,onFocus:()=>a.value=!0,onBlur:()=>a.value=!1,onInput:P=>c.value=P.target.value,onKeydown:P=>{switch(P.key){case"ArrowUp":{C();break}case"ArrowDown":{v();break}case"Enter":{P.preventDefault(),b(h.value);break}}}}),w.value&&ae("ul",{class:"suggestions",onMouseleave:()=>h.value=-1},f.value.map(({link:P,title:S,header:U},Z)=>ae("li",{class:["suggestion",{focus:h.value===Z}],onMouseenter:()=>h.value=Z,onMousedown:()=>b(Z)},ae("a",{href:P,onClick:$=>$.preventDefault()},[ae("span",{class:"page-title"},S),U&&ae("span",{class:"page-header"},`> ${U}`)]))))])}});const Sm={"/":{placeholder:"Search"},"/zh/":{placeholder:"搜索"}},Pm=["s","/"],Am=5,Om=Dt({enhance({app:e}){e.component("SearchBox",t=>ae(Tm,{locales:Sm,hotKeys:Pm,maxSuggestions:Am,...t}))}}),fl=()=>window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0,Rm=()=>window.scrollTo({top:0,behavior:"smooth"});const Im=ue({name:"BackToTop",setup(){const e=me(0),t=F(()=>e.value>300),n=Ei(()=>{e.value=fl()},100);Je(()=>{e.value=fl(),window.addEventListener("scroll",()=>n())});const r=ae("div",{class:"back-to-top",onClick:Rm});return()=>ae(qn,{name:"back-to-top"},()=>t.value?r:null)}}),$m=Dt({rootComponents:[Im]}),lr=[sd,cd,Ed,Ld,Ad,gm,Om,$m],Nm=[["v-fe360bd6","/develop.html",{title:""},["/develop","/develop.md"]],["v-d08d435a","/introduce.html",{title:"介绍"},["/introduce","/introduce.md"]],["v-8daa1a0e","/",{title:""},["/index.html","/README.md"]],["v-53c50d87","/app-testing/appium_lab.html",{title:"appium API"},["/app-testing/appium_lab","/app-testing/appium_lab.md"]],["v-483ce5fe","/app-testing/extensions.html",{title:"appium 扩展"},["/app-testing/extensions","/app-testing/extensions.md"]],["v-299549e4","/app-testing/page_object.html",{title:"Page Object"},["/app-testing/page_object","/app-testing/page_object.md"]],["v-4370387b","/app-testing/start.html",{title:"app 测试"},["/app-testing/start","/app-testing/start.md"]],["v-7cbb39b9","/api-testing/api_case.html",{title:"支持Excel测试用例"},["/api-testing/api_case","/api-testing/api_case.md"]],["v-109501ec","/api-testing/api_object.html",{title:"API Object"},["/api-testing/api_object","/api-testing/api_object.md"]],["v-d24a86f0","/api-testing/assert.html",{title:"更强大的断言"},["/api-testing/assert","/api-testing/assert.md"]],["v-5b62ef19","/api-testing/more.html",{title:"更多功能"},["/api-testing/more","/api-testing/more.md"]],["v-23f9483c","/api-testing/start.html",{title:"开始使用"},["/api-testing/start","/api-testing/start.md"]],["v-488b8cec","/api-testing/webscocket.html",{title:"WebSocket"},["/api-testing/webscocket","/api-testing/webscocket.md"]],["v-d8f79a72","/getting-started/advanced.html",{title:"高级用法"},["/getting-started/advanced","/getting-started/advanced.md"]],["v-8e520eda","/getting-started/create_project.html",{title:"创建项目"},["/getting-started/create_project","/getting-started/create_project.md"]],["v-78c619cc","/getting-started/data_driver.html",{title:"数据驱动"},["/getting-started/data_driver","/getting-started/data_driver.md"]],["v-4e6f0425","/getting-started/dependent_func.html",{title:"方法的依赖"},["/getting-started/dependent_func","/getting-started/dependent_func.md"]],["v-4e8563af","/getting-started/installation.html",{title:"Installation"},["/getting-started/installation","/getting-started/installation.md"]],["v-0f898c79","/getting-started/quick_start.html",{title:"快速开始"},["/getting-started/quick_start","/getting-started/quick_start.md"]],["v-6f32df80","/getting-started/seldom_cli.html",{title:"seldom CLI"},["/getting-started/seldom_cli","/getting-started/seldom_cli.md"]],["v-3cdc5c3a","/platform/platform.html",{title:"平台化支持"},["/platform/platform","/platform/platform.md"]],["v-c350a662","/more-ability/db_operation.html",{title:"数据库操作"},["/more-ability/db_operation","/more-ability/db_operation.md"]],["v-1d715ea7","/more-ability/test_library.html",{title:"支持更多测试库"},["/more-ability/test_library","/more-ability/test_library.md"]],["v-471218ee","/web-testing/browser_driver.html",{title:"浏览器与驱动"},["/web-testing/browser_driver","/web-testing/browser_driver.md"]],["v-198befa7","/web-testing/chaining.html",{title:"链式调用"},["/web-testing/chaining","/web-testing/chaining.md"]],["v-1f2c830c","/web-testing/other.html",{title:"浏览器启动配置"},["/web-testing/other","/web-testing/other.md"]],["v-0ccdb93b","/web-testing/page_object.html",{title:"Page Object"},["/web-testing/page_object","/web-testing/page_object.md"]],["v-40ed12b6","/web-testing/seldom_api.html",{title:"Seldom API"},["/web-testing/seldom_api","/web-testing/seldom_api.md"]],["v-129a7066","/version/CHANGES.html",{title:"版本更新"},["/version/CHANGES","/version/CHANGES.md"]],["v-3706649a","/404.html",{title:""},["/404"]]];var dl=ue({name:"Vuepress",setup(){const e=Wu();return()=>ae(e.value)}}),Dm=()=>Nm.reduce((e,[t,n,r,o])=>(e.push({name:t,path:n,component:dl,meta:r},...o.map(s=>({path:s,redirect:n}))),e),[{name:"404",path:"/:catchAll(.*)",component:dl}]),Mm=mf,Hm=()=>{const e=Zf({history:Mm(ui("/")),routes:Dm(),scrollBehavior:(t,n,r)=>r||(t.hash?{el:t.hash}:{top:0})});return e.beforeResolve(async(t,n)=>{var r;(t.path!==n.path||n===pt)&&([St.value]=await Promise.all([ht.resolvePageData(t.name),(r=hi[t.name])==null?void 0:r.__asyncLoader()]))}),e},Fm=e=>{e.component("ClientOnly",Ho),e.component("Content",Ju)},jm=(e,t,n)=>{const r=me(t.currentRoute.value.path);st(()=>t.currentRoute.value.path,h=>r.value=h);const o=F(()=>ht.resolveLayouts(n)),s=F(()=>ht.resolveRouteLocale(nn.value.locales,r.value)),l=F(()=>ht.resolveSiteLocaleData(nn.value,s.value)),i=F(()=>ht.resolvePageFrontmatter(St.value)),a=F(()=>ht.resolvePageHeadTitle(St.value,l.value)),c=F(()=>ht.resolvePageHead(a.value,i.value,l.value)),u=F(()=>ht.resolvePageLang(St.value)),f=F(()=>ht.resolvePageLayout(St.value,o.value));return e.provide(Bu,o),e.provide(mi,i),e.provide(Uu,a),e.provide(gi,c),e.provide(vi,u),e.provide(_i,f),e.provide(Do,s),e.provide(yi,l),Object.defineProperties(e.config.globalProperties,{$frontmatter:{get:()=>i.value},$head:{get:()=>c.value},$headTitle:{get:()=>a.value},$lang:{get:()=>u.value},$page:{get:()=>St.value},$routeLocale:{get:()=>s.value},$site:{get:()=>nn.value},$siteLocale:{get:()=>l.value},$withBase:{get:()=>Fo}}),{layouts:o,pageData:St,pageFrontmatter:i,pageHead:c,pageHeadTitle:a,pageLang:u,pageLayout:f,routeLocale:s,siteData:nn,siteLocaleData:l}},Bm=()=>{const e=Vu(),t=qu(),n=me([]),r=()=>{e.value.forEach(s=>{const l=zm(s);l&&n.value.push(l)})},o=()=>{document.documentElement.lang=t.value,n.value.forEach(s=>{s.parentNode===document.head&&document.head.removeChild(s)}),n.value.splice(0,n.value.length),e.value.forEach(s=>{const l=Vm(s);l!==null&&(document.head.appendChild(l),n.value.push(l))})};Wt(Ku,o),Je(()=>{r(),o(),st(()=>e.value,()=>o())})},zm=([e,t,n=""])=>{const r=Object.entries(t).map(([i,a])=>ge(a)?`[${i}=${JSON.stringify(a)}]`:a===!0?`[${i}]`:"").join(""),o=`head > ${e}${r}`;return Array.from(document.querySelectorAll(o)).find(i=>i.innerText===n)||null},Vm=([e,t,n])=>{if(!ge(e))return null;const r=document.createElement(e);return No(t)&&Object.entries(t).forEach(([o,s])=>{ge(s)?r.setAttribute(o,s):s===!0&&r.setAttribute(o,"")}),ge(n)&&r.appendChild(document.createTextNode(n)),r},Um=Iu,qm=async()=>{var n;const e=Um({name:"VuepressApp",setup(){var r;Bm();for(const o of lr)(r=o.setup)==null||r.call(o);return()=>[ae(Ri),...lr.flatMap(({rootComponents:o=[]})=>o.map(s=>ae(s)))]}}),t=Hm();Fm(e),jm(e,t,lr);for(const r of lr)await((n=r.enhance)==null?void 0:n.call(r,{app:e,router:t,siteData:nn}));return e.use(t),{app:e,router:t}};qm().then(({app:e,router:t})=>{t.isReady().then(()=>{e.mount("#app")})});export{Le as _,he as a,Nt as b,X as c,qm as createVueApp,te as d,Gc as e,j as o,bt as r}; diff --git a/assets/app-a06a2d51.js b/assets/app-a06a2d51.js deleted file mode 100644 index 05cc07f..0000000 --- a/assets/app-a06a2d51.js +++ /dev/null @@ -1,10 +0,0 @@ -const Qi="modulepreload",Zi=function(e){return"/"+e},Jo={},W=function(t,n,r){if(!n||n.length===0)return t();const o=document.getElementsByTagName("link");return Promise.all(n.map(s=>{if(s=Zi(s),s in Jo)return;Jo[s]=!0;const l=s.endsWith(".css"),i=l?'[rel="stylesheet"]':"";if(!!r)for(let u=o.length-1;u>=0;u--){const f=o[u];if(f.href===s&&(!l||f.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${s}"]${i}`))return;const c=document.createElement("link");if(c.rel=l?"stylesheet":Qi,l||(c.as="script",c.crossOrigin=""),c.href=s,document.head.appendChild(c),l)return new Promise((u,f)=>{c.addEventListener("load",u),c.addEventListener("error",()=>f(new Error(`Unable to preload CSS for ${s}`)))})})).then(()=>t())};function mo(e,t){const n=Object.create(null),r=e.split(",");for(let o=0;o!!n[o.toLowerCase()]:o=>!!n[o]}const xe={},rn=[],ot=()=>{},Xi=()=>!1,ea=/^on[^a-z]/,Fn=e=>ea.test(e),go=e=>e.startsWith("onUpdate:"),Ae=Object.assign,vo=(e,t)=>{const n=e.indexOf(t);n>-1&&e.splice(n,1)},ta=Object.prototype.hasOwnProperty,de=(e,t)=>ta.call(e,t),Q=Array.isArray,on=e=>kr(e)==="[object Map]",pl=e=>kr(e)==="[object Set]",oe=e=>typeof e=="function",ge=e=>typeof e=="string",_o=e=>typeof e=="symbol",ke=e=>e!==null&&typeof e=="object",ml=e=>ke(e)&&oe(e.then)&&oe(e.catch),gl=Object.prototype.toString,kr=e=>gl.call(e),na=e=>kr(e).slice(8,-1),vl=e=>kr(e)==="[object Object]",bo=e=>ge(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,Ln=mo(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),xr=e=>{const t=Object.create(null);return n=>t[n]||(t[n]=e(n))},ra=/-(\w)/g,ft=xr(e=>e.replace(ra,(t,n)=>n?n.toUpperCase():"")),oa=/\B([A-Z])/g,Yt=xr(e=>e.replace(oa,"-$1").toLowerCase()),Lr=xr(e=>e.charAt(0).toUpperCase()+e.slice(1)),Mr=xr(e=>e?`on${Lr(e)}`:""),On=(e,t)=>!Object.is(e,t),Hr=(e,t)=>{for(let n=0;n{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,value:n})},sa=e=>{const t=parseFloat(e);return isNaN(t)?e:t},la=e=>{const t=ge(e)?Number(e):NaN;return isNaN(t)?e:t};let Qo;const Zr=()=>Qo||(Qo=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function jn(e){if(Q(e)){const t={};for(let n=0;n{if(n){const r=n.split(aa);r.length>1&&(t[r[0].trim()]=r[1].trim())}}),t}function qe(e){let t="";if(ge(e))t=e;else if(Q(e))for(let n=0;nge(e)?e:e==null?"":Q(e)||ke(e)&&(e.toString===gl||!oe(e.toString))?JSON.stringify(e,bl,2):String(e),bl=(e,t)=>t&&t.__v_isRef?bl(e,t.value):on(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((n,[r,o])=>(n[`${r} =>`]=o,n),{})}:pl(t)?{[`Set(${t.size})`]:[...t.values()]}:ke(t)&&!Q(t)&&!vl(t)?String(t):t;let Ge;class ha{constructor(t=!1){this.detached=t,this._active=!0,this.effects=[],this.cleanups=[],this.parent=Ge,!t&&Ge&&(this.index=(Ge.scopes||(Ge.scopes=[])).push(this)-1)}get active(){return this._active}run(t){if(this._active){const n=Ge;try{return Ge=this,t()}finally{Ge=n}}}on(){Ge=this}off(){Ge=this.parent}stop(t){if(this._active){let n,r;for(n=0,r=this.effects.length;n{const t=new Set(e);return t.w=0,t.n=0,t},El=e=>(e.w&Rt)>0,wl=e=>(e.n&Rt)>0,ga=({deps:e})=>{if(e.length)for(let t=0;t{const{deps:t}=e;if(t.length){let n=0;for(let r=0;r{(u==="length"||u>=a)&&i.push(c)})}else switch(n!==void 0&&i.push(l.get(n)),t){case"add":Q(e)?bo(n)&&i.push(l.get("length")):(i.push(l.get(qt)),on(e)&&i.push(l.get(eo)));break;case"delete":Q(e)||(i.push(l.get(qt)),on(e)&&i.push(l.get(eo)));break;case"set":on(e)&&i.push(l.get(qt));break}if(i.length===1)i[0]&&to(i[0]);else{const a=[];for(const c of i)c&&a.push(...c);to(yo(a))}}function to(e,t){const n=Q(e)?e:[...e];for(const r of n)r.computed&&Xo(r);for(const r of n)r.computed||Xo(r)}function Xo(e,t){(e!==nt||e.allowRecurse)&&(e.scheduler?e.scheduler():e.run())}function _a(e,t){var n;return(n=ur.get(e))==null?void 0:n.get(t)}const ba=mo("__proto__,__v_isRef,__isVue"),Ll=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(_o)),ya=wo(),Ea=wo(!1,!0),wa=wo(!0),es=ka();function ka(){const e={};return["includes","indexOf","lastIndexOf"].forEach(t=>{e[t]=function(...n){const r=pe(this);for(let s=0,l=this.length;s{e[t]=function(...n){mn();const r=pe(this)[t].apply(this,n);return gn(),r}}),e}function xa(e){const t=pe(this);return We(t,"has",e),t.hasOwnProperty(e)}function wo(e=!1,t=!1){return function(r,o,s){if(o==="__v_isReactive")return!e;if(o==="__v_isReadonly")return e;if(o==="__v_isShallow")return t;if(o==="__v_raw"&&s===(e?t?ja:Al:t?Pl:Sl).get(r))return r;const l=Q(r);if(!e){if(l&&de(es,o))return Reflect.get(es,o,s);if(o==="hasOwnProperty")return xa}const i=Reflect.get(r,o,s);return(_o(o)?Ll.has(o):ba(o))||(e||We(r,"get",o),t)?i:Ie(i)?l&&bo(o)?i:i.value:ke(i)?e?Bn(i):vn(i):i}}const La=Cl(),Ca=Cl(!0);function Cl(e=!1){return function(n,r,o,s){let l=n[r];if(cn(l)&&Ie(l)&&!Ie(o))return!1;if(!e&&(!fr(o)&&!cn(o)&&(l=pe(l),o=pe(o)),!Q(n)&&Ie(l)&&!Ie(o)))return l.value=o,!0;const i=Q(n)&&bo(r)?Number(r)e,Cr=e=>Reflect.getPrototypeOf(e);function Gn(e,t,n=!1,r=!1){e=e.__v_raw;const o=pe(e),s=pe(t);n||(t!==s&&We(o,"get",t),We(o,"get",s));const{has:l}=Cr(o),i=r?ko:n?Co:Rn;if(l.call(o,t))return i(e.get(t));if(l.call(o,s))return i(e.get(s));e!==o&&e.get(t)}function Yn(e,t=!1){const n=this.__v_raw,r=pe(n),o=pe(e);return t||(e!==o&&We(r,"has",e),We(r,"has",o)),e===o?n.has(e):n.has(e)||n.has(o)}function Jn(e,t=!1){return e=e.__v_raw,!t&&We(pe(e),"iterate",qt),Reflect.get(e,"size",e)}function ts(e){e=pe(e);const t=pe(this);return Cr(t).has.call(t,e)||(t.add(e),_t(t,"add",e,e)),this}function ns(e,t){t=pe(t);const n=pe(this),{has:r,get:o}=Cr(n);let s=r.call(n,e);s||(e=pe(e),s=r.call(n,e));const l=o.call(n,e);return n.set(e,t),s?On(t,l)&&_t(n,"set",e,t):_t(n,"add",e,t),this}function rs(e){const t=pe(this),{has:n,get:r}=Cr(t);let o=n.call(t,e);o||(e=pe(e),o=n.call(t,e)),r&&r.call(t,e);const s=t.delete(e);return o&&_t(t,"delete",e,void 0),s}function os(){const e=pe(this),t=e.size!==0,n=e.clear();return t&&_t(e,"clear",void 0,void 0),n}function Qn(e,t){return function(r,o){const s=this,l=s.__v_raw,i=pe(l),a=t?ko:e?Co:Rn;return!e&&We(i,"iterate",qt),l.forEach((c,u)=>r.call(o,a(c),a(u),s))}}function Zn(e,t,n){return function(...r){const o=this.__v_raw,s=pe(o),l=on(s),i=e==="entries"||e===Symbol.iterator&&l,a=e==="keys"&&l,c=o[e](...r),u=n?ko:t?Co:Rn;return!t&&We(s,"iterate",a?eo:qt),{next(){const{value:f,done:h}=c.next();return h?{value:f,done:h}:{value:i?[u(f[0]),u(f[1])]:u(f),done:h}},[Symbol.iterator](){return this}}}}function kt(e){return function(...t){return e==="delete"?!1:this}}function Ra(){const e={get(s){return Gn(this,s)},get size(){return Jn(this)},has:Yn,add:ts,set:ns,delete:rs,clear:os,forEach:Qn(!1,!1)},t={get(s){return Gn(this,s,!1,!0)},get size(){return Jn(this)},has:Yn,add:ts,set:ns,delete:rs,clear:os,forEach:Qn(!1,!0)},n={get(s){return Gn(this,s,!0)},get size(){return Jn(this,!0)},has(s){return Yn.call(this,s,!0)},add:kt("add"),set:kt("set"),delete:kt("delete"),clear:kt("clear"),forEach:Qn(!0,!1)},r={get(s){return Gn(this,s,!0,!0)},get size(){return Jn(this,!0)},has(s){return Yn.call(this,s,!0)},add:kt("add"),set:kt("set"),delete:kt("delete"),clear:kt("clear"),forEach:Qn(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(s=>{e[s]=Zn(s,!1,!1),n[s]=Zn(s,!0,!1),t[s]=Zn(s,!1,!0),r[s]=Zn(s,!0,!0)}),[e,n,t,r]}const[Ia,$a,Na,Da]=Ra();function xo(e,t){const n=t?e?Da:Na:e?$a:Ia;return(r,o,s)=>o==="__v_isReactive"?!e:o==="__v_isReadonly"?e:o==="__v_raw"?r:Reflect.get(de(n,o)&&o in r?n:r,o,s)}const Ma={get:xo(!1,!1)},Ha={get:xo(!1,!0)},Fa={get:xo(!0,!1)},Sl=new WeakMap,Pl=new WeakMap,Al=new WeakMap,ja=new WeakMap;function Ba(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function za(e){return e.__v_skip||!Object.isExtensible(e)?0:Ba(na(e))}function vn(e){return cn(e)?e:Lo(e,!1,Tl,Ma,Sl)}function Va(e){return Lo(e,!1,Oa,Ha,Pl)}function Bn(e){return Lo(e,!0,Aa,Fa,Al)}function Lo(e,t,n,r,o){if(!ke(e)||e.__v_raw&&!(t&&e.__v_isReactive))return e;const s=o.get(e);if(s)return s;const l=za(e);if(l===0)return e;const i=new Proxy(e,l===2?r:n);return o.set(e,i),i}function sn(e){return cn(e)?sn(e.__v_raw):!!(e&&e.__v_isReactive)}function cn(e){return!!(e&&e.__v_isReadonly)}function fr(e){return!!(e&&e.__v_isShallow)}function Ol(e){return sn(e)||cn(e)}function pe(e){const t=e&&e.__v_raw;return t?pe(t):e}function Rl(e){return cr(e,"__v_skip",!0),e}const Rn=e=>ke(e)?vn(e):e,Co=e=>ke(e)?Bn(e):e;function To(e){At&&nt&&(e=pe(e),xl(e.dep||(e.dep=yo())))}function So(e,t){e=pe(e);const n=e.dep;n&&to(n)}function Ie(e){return!!(e&&e.__v_isRef===!0)}function me(e){return $l(e,!1)}function Il(e){return $l(e,!0)}function $l(e,t){return Ie(e)?e:new Ua(e,t)}class Ua{constructor(t,n){this.__v_isShallow=n,this.dep=void 0,this.__v_isRef=!0,this._rawValue=n?t:pe(t),this._value=n?t:Rn(t)}get value(){return To(this),this._value}set value(t){const n=this.__v_isShallow||fr(t)||cn(t);t=n?t:pe(t),On(t,this._rawValue)&&(this._rawValue=t,this._value=n?t:Rn(t),So(this))}}function ee(e){return Ie(e)?e.value:e}const qa={get:(e,t,n)=>ee(Reflect.get(e,t,n)),set:(e,t,n,r)=>{const o=e[t];return Ie(o)&&!Ie(n)?(o.value=n,!0):Reflect.set(e,t,n,r)}};function Nl(e){return sn(e)?e:new Proxy(e,qa)}class Wa{constructor(t){this.dep=void 0,this.__v_isRef=!0;const{get:n,set:r}=t(()=>To(this),()=>So(this));this._get=n,this._set=r}get value(){return this._get()}set value(t){this._set(t)}}function Ka(e){return new Wa(e)}function Tr(e){const t=Q(e)?new Array(e.length):{};for(const n in e)t[n]=Dl(e,n);return t}class Ga{constructor(t,n,r){this._object=t,this._key=n,this._defaultValue=r,this.__v_isRef=!0}get value(){const t=this._object[this._key];return t===void 0?this._defaultValue:t}set value(t){this._object[this._key]=t}get dep(){return _a(pe(this._object),this._key)}}class Ya{constructor(t){this._getter=t,this.__v_isRef=!0,this.__v_isReadonly=!0}get value(){return this._getter()}}function Ja(e,t,n){return Ie(e)?e:oe(e)?new Ya(e):ke(e)&&arguments.length>1?Dl(e,t,n):me(e)}function Dl(e,t,n){const r=e[t];return Ie(r)?r:new Ga(e,t,n)}class Qa{constructor(t,n,r,o){this._setter=n,this.dep=void 0,this.__v_isRef=!0,this.__v_isReadonly=!1,this._dirty=!0,this.effect=new Eo(t,()=>{this._dirty||(this._dirty=!0,So(this))}),this.effect.computed=this,this.effect.active=this._cacheable=!o,this.__v_isReadonly=r}get value(){const t=pe(this);return To(t),(t._dirty||!t._cacheable)&&(t._dirty=!1,t._value=t.effect.run()),t._value}set value(t){this._setter(t)}}function Za(e,t,n=!1){let r,o;const s=oe(e);return s?(r=e,o=ot):(r=e.get,o=e.set),new Qa(r,o,s||!o,n)}function Ot(e,t,n,r){let o;try{o=r?e(...r):e()}catch(s){zn(s,t,n)}return o}function Xe(e,t,n,r){if(oe(e)){const s=Ot(e,t,n,r);return s&&ml(s)&&s.catch(l=>{zn(l,t,n)}),s}const o=[];for(let s=0;s>>1;$n(je[r])ut&&je.splice(t,1)}function nc(e){Q(e)?ln.push(...e):(!mt||!mt.includes(e,e.allowRecurse?Bt+1:Bt))&&ln.push(e),Hl()}function ss(e,t=In?ut+1:0){for(;t$n(n)-$n(r)),Bt=0;Bte.id==null?1/0:e.id,rc=(e,t)=>{const n=$n(e)-$n(t);if(n===0){if(e.pre&&!t.pre)return-1;if(t.pre&&!e.pre)return 1}return n};function Fl(e){no=!1,In=!0,je.sort(rc);const t=ot;try{for(ut=0;utge(g)?g.trim():g)),f&&(o=n.map(sa))}let i,a=r[i=Mr(t)]||r[i=Mr(ft(t))];!a&&s&&(a=r[i=Mr(Yt(t))]),a&&Xe(a,e,6,o);const c=r[i+"Once"];if(c){if(!e.emitted)e.emitted={};else if(e.emitted[i])return;e.emitted[i]=!0,Xe(c,e,6,o)}}function jl(e,t,n=!1){const r=t.emitsCache,o=r.get(e);if(o!==void 0)return o;const s=e.emits;let l={},i=!1;if(!oe(e)){const a=c=>{const u=jl(c,t,!0);u&&(i=!0,Ae(l,u))};!n&&t.mixins.length&&t.mixins.forEach(a),e.extends&&a(e.extends),e.mixins&&e.mixins.forEach(a)}return!s&&!i?(ke(e)&&r.set(e,null),null):(Q(s)?s.forEach(a=>l[a]=null):Ae(l,s),ke(e)&&r.set(e,l),l)}function Ar(e,t){return!e||!Fn(t)?!1:(t=t.slice(2).replace(/Once$/,""),de(e,t[0].toLowerCase()+t.slice(1))||de(e,Yt(t))||de(e,t))}let Me=null,Bl=null;function hr(e){const t=Me;return Me=e,Bl=e&&e.type.__scopeId||null,t}function De(e,t=Me,n){if(!t||e._n)return e;const r=(...o)=>{r._d&&vs(-1);const s=hr(t);let l;try{l=e(...o)}finally{hr(s),r._d&&vs(1)}return l};return r._n=!0,r._c=!0,r._d=!0,r}function Fr(e){const{type:t,vnode:n,proxy:r,withProxy:o,props:s,propsOptions:[l],slots:i,attrs:a,emit:c,render:u,renderCache:f,data:h,setupState:g,ctx:y,inheritAttrs:w}=e;let C,v;const b=hr(e);try{if(n.shapeFlag&4){const S=o||r;C=tt(u.call(S,S,f,s,g,h,y)),v=a}else{const S=t;C=tt(S.length>1?S(s,{attrs:a,slots:i,emit:c}):S(s,null)),v=t.props?a:sc(a)}}catch(S){Sn.length=0,zn(S,e,1),C=te(Ye)}let P=C;if(v&&w!==!1){const S=Object.keys(v),{shapeFlag:U}=P;S.length&&U&7&&(l&&S.some(go)&&(v=lc(v,l)),P=$t(P,v))}return n.dirs&&(P=$t(P),P.dirs=P.dirs?P.dirs.concat(n.dirs):n.dirs),n.transition&&(P.transition=n.transition),C=P,hr(b),C}const sc=e=>{let t;for(const n in e)(n==="class"||n==="style"||Fn(n))&&((t||(t={}))[n]=e[n]);return t},lc=(e,t)=>{const n={};for(const r in e)(!go(r)||!(r.slice(9)in t))&&(n[r]=e[r]);return n};function ic(e,t,n){const{props:r,children:o,component:s}=e,{props:l,children:i,patchFlag:a}=t,c=s.emitsOptions;if(t.dirs||t.transition)return!0;if(n&&a>=0){if(a&1024)return!0;if(a&16)return r?ls(r,l,c):!!l;if(a&8){const u=t.dynamicProps;for(let f=0;fe.__isSuspense;function zl(e,t){t&&t.pendingBranch?Q(e)?t.effects.push(...e):t.effects.push(e):nc(e)}function uc(e,t){return Ao(e,null,t)}const Xn={};function st(e,t,n){return Ao(e,t,n)}function Ao(e,t,{immediate:n,deep:r,flush:o,onTrack:s,onTrigger:l}=xe){var i;const a=yl()===((i=Oe)==null?void 0:i.scope)?Oe:null;let c,u=!1,f=!1;if(Ie(e)?(c=()=>e.value,u=fr(e)):sn(e)?(c=()=>e,r=!0):Q(e)?(f=!0,u=e.some(S=>sn(S)||fr(S)),c=()=>e.map(S=>{if(Ie(S))return S.value;if(sn(S))return Ut(S);if(oe(S))return Ot(S,a,2)})):oe(e)?t?c=()=>Ot(e,a,2):c=()=>{if(!(a&&a.isUnmounted))return h&&h(),Xe(e,a,3,[g])}:c=ot,t&&r){const S=c;c=()=>Ut(S())}let h,g=S=>{h=b.onStop=()=>{Ot(S,a,4)}},y;if(dn)if(g=ot,t?n&&Xe(t,a,3,[c(),f?[]:void 0,g]):c(),o==="sync"){const S=su();y=S.__watcherHandles||(S.__watcherHandles=[])}else return ot;let w=f?new Array(e.length).fill(Xn):Xn;const C=()=>{if(b.active)if(t){const S=b.run();(r||u||(f?S.some((U,Z)=>On(U,w[Z])):On(S,w)))&&(h&&h(),Xe(t,a,3,[S,w===Xn?void 0:f&&w[0]===Xn?[]:w,g]),w=S)}else b.run()};C.allowRecurse=!!t;let v;o==="sync"?v=C:o==="post"?v=()=>Ue(C,a&&a.suspense):(C.pre=!0,a&&(C.id=a.uid),v=()=>Pr(C));const b=new Eo(c,v);t?n?C():w=b.run():o==="post"?Ue(b.run.bind(b),a&&a.suspense):b.run();const P=()=>{b.stop(),a&&a.scope&&vo(a.scope.effects,b)};return y&&y.push(P),P}function fc(e,t,n){const r=this.proxy,o=ge(e)?e.includes(".")?Vl(r,e):()=>r[e]:e.bind(r,r);let s;oe(t)?s=t:(s=t.handler,n=t);const l=Oe;fn(this);const i=Ao(o,s.bind(r),n);return l?fn(l):Kt(),i}function Vl(e,t){const n=t.split(".");return()=>{let r=e;for(let o=0;o{Ut(n,t)});else if(vl(e))for(const n in e)Ut(e[n],t);return e}function pr(e,t){const n=Me;if(n===null)return e;const r=$r(n)||n.proxy,o=e.dirs||(e.dirs=[]);for(let s=0;s{e.isMounted=!0}),Un(()=>{e.isUnmounting=!0}),e}const Qe=[Function,Array],Ul={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:Qe,onEnter:Qe,onAfterEnter:Qe,onEnterCancelled:Qe,onBeforeLeave:Qe,onLeave:Qe,onAfterLeave:Qe,onLeaveCancelled:Qe,onBeforeAppear:Qe,onAppear:Qe,onAfterAppear:Qe,onAppearCancelled:Qe},hc={name:"BaseTransition",props:Ul,setup(e,{slots:t}){const n=li(),r=dc();let o;return()=>{const s=t.default&&Wl(t.default(),!0);if(!s||!s.length)return;let l=s[0];if(s.length>1){for(const w of s)if(w.type!==Ye){l=w;break}}const i=pe(e),{mode:a}=i;if(r.isLeaving)return jr(l);const c=is(l);if(!c)return jr(l);const u=ro(c,i,r,n);oo(c,u);const f=n.subTree,h=f&&is(f);let g=!1;const{getTransitionKey:y}=c.type;if(y){const w=y();o===void 0?o=w:w!==o&&(o=w,g=!0)}if(h&&h.type!==Ye&&(!zt(c,h)||g)){const w=ro(h,i,r,n);if(oo(h,w),a==="out-in")return r.isLeaving=!0,w.afterLeave=()=>{r.isLeaving=!1,n.update.active!==!1&&n.update()},jr(l);a==="in-out"&&c.type!==Ye&&(w.delayLeave=(C,v,b)=>{const P=ql(r,h);P[String(h.key)]=h,C._leaveCb=()=>{v(),C._leaveCb=void 0,delete u.delayedLeave},u.delayedLeave=b})}return l}}},pc=hc;function ql(e,t){const{leavingVNodes:n}=e;let r=n.get(t.type);return r||(r=Object.create(null),n.set(t.type,r)),r}function ro(e,t,n,r){const{appear:o,mode:s,persisted:l=!1,onBeforeEnter:i,onEnter:a,onAfterEnter:c,onEnterCancelled:u,onBeforeLeave:f,onLeave:h,onAfterLeave:g,onLeaveCancelled:y,onBeforeAppear:w,onAppear:C,onAfterAppear:v,onAppearCancelled:b}=t,P=String(e.key),S=ql(n,e),U=(m,z)=>{m&&Xe(m,r,9,z)},Z=(m,z)=>{const M=z[1];U(m,z),Q(m)?m.every(G=>G.length<=1)&&M():m.length<=1&&M()},$={mode:s,persisted:l,beforeEnter(m){let z=i;if(!n.isMounted)if(o)z=w||i;else return;m._leaveCb&&m._leaveCb(!0);const M=S[P];M&&zt(e,M)&&M.el._leaveCb&&M.el._leaveCb(),U(z,[m])},enter(m){let z=a,M=c,G=u;if(!n.isMounted)if(o)z=C||a,M=v||c,G=b||u;else return;let L=!1;const R=m._enterCb=I=>{L||(L=!0,I?U(G,[m]):U(M,[m]),$.delayedLeave&&$.delayedLeave(),m._enterCb=void 0)};z?Z(z,[m,R]):R()},leave(m,z){const M=String(e.key);if(m._enterCb&&m._enterCb(!0),n.isUnmounting)return z();U(f,[m]);let G=!1;const L=m._leaveCb=R=>{G||(G=!0,z(),R?U(y,[m]):U(g,[m]),m._leaveCb=void 0,S[M]===e&&delete S[M])};S[M]=e,h?Z(h,[m,L]):L()},clone(m){return ro(m,t,n,r)}};return $}function jr(e){if(Vn(e))return e=$t(e),e.children=null,e}function is(e){return Vn(e)?e.children?e.children[0]:void 0:e}function oo(e,t){e.shapeFlag&6&&e.component?oo(e.component.subTree,t):e.shapeFlag&128?(e.ssContent.transition=t.clone(e.ssContent),e.ssFallback.transition=t.clone(e.ssFallback)):e.transition=t}function Wl(e,t=!1,n){let r=[],o=0;for(let s=0;s1)for(let s=0;sAe({name:e.name},t,{setup:e}))():e}const an=e=>!!e.type.__asyncLoader;function ve(e){oe(e)&&(e={loader:e});const{loader:t,loadingComponent:n,errorComponent:r,delay:o=200,timeout:s,suspensible:l=!0,onError:i}=e;let a=null,c,u=0;const f=()=>(u++,a=null,h()),h=()=>{let g;return a||(g=a=t().catch(y=>{if(y=y instanceof Error?y:new Error(String(y)),i)return new Promise((w,C)=>{i(y,()=>w(f()),()=>C(y),u+1)});throw y}).then(y=>g!==a&&a?a:(y&&(y.__esModule||y[Symbol.toStringTag]==="Module")&&(y=y.default),c=y,y)))};return ue({name:"AsyncComponentWrapper",__asyncLoader:h,get __asyncResolved(){return c},setup(){const g=Oe;if(c)return()=>Br(c,g);const y=b=>{a=null,zn(b,g,13,!r)};if(l&&g.suspense||dn)return h().then(b=>()=>Br(b,g)).catch(b=>(y(b),()=>r?te(r,{error:b}):null));const w=me(!1),C=me(),v=me(!!o);return o&&setTimeout(()=>{v.value=!1},o),s!=null&&setTimeout(()=>{if(!w.value&&!C.value){const b=new Error(`Async component timed out after ${s}ms.`);y(b),C.value=b}},s),h().then(()=>{w.value=!0,g.parent&&Vn(g.parent.vnode)&&Pr(g.parent.update)}).catch(b=>{y(b),C.value=b}),()=>{if(w.value&&c)return Br(c,g);if(C.value&&r)return te(r,{error:C.value});if(n&&!v.value)return te(n)}}})}function Br(e,t){const{ref:n,props:r,children:o,ce:s}=t.vnode,l=te(e,r,o);return l.ref=n,l.ce=s,delete t.vnode.ce,l}const Vn=e=>e.type.__isKeepAlive;function mc(e,t){Kl(e,"a",t)}function gc(e,t){Kl(e,"da",t)}function Kl(e,t,n=Oe){const r=e.__wdc||(e.__wdc=()=>{let o=n;for(;o;){if(o.isDeactivated)return;o=o.parent}return e()});if(Or(t,r,n),n){let o=n.parent;for(;o&&o.parent;)Vn(o.parent.vnode)&&vc(r,t,n,o),o=o.parent}}function vc(e,t,n,r){const o=Or(t,e,r,!0);Rr(()=>{vo(r[t],o)},n)}function Or(e,t,n=Oe,r=!1){if(n){const o=n[e]||(n[e]=[]),s=t.__weh||(t.__weh=(...l)=>{if(n.isUnmounted)return;mn(),fn(n);const i=Xe(t,n,e,l);return Kt(),gn(),i});return r?o.unshift(s):o.push(s),s}}const yt=e=>(t,n=Oe)=>(!dn||e==="sp")&&Or(e,(...r)=>t(...r),n),_c=yt("bm"),Je=yt("m"),bc=yt("bu"),yc=yt("u"),Un=yt("bum"),Rr=yt("um"),Ec=yt("sp"),wc=yt("rtg"),kc=yt("rtc");function xc(e,t=Oe){Or("ec",e,t)}const Gl="components";function bt(e,t){return Cc(Gl,e,!0,t)||e}const Lc=Symbol.for("v-ndc");function Cc(e,t,n=!0,r=!1){const o=Me||Oe;if(o){const s=o.type;if(e===Gl){const i=nu(s,!1);if(i&&(i===t||i===ft(t)||i===Lr(ft(t))))return s}const l=as(o[e]||s[e],t)||as(o.appContext[e],t);return!l&&r?s:l}}function as(e,t){return e&&(e[t]||e[ft(t)]||e[Lr(ft(t))])}function It(e,t,n,r){let o;const s=n&&n[r];if(Q(e)||ge(e)){o=new Array(e.length);for(let l=0,i=e.length;lt(l,i,void 0,s&&s[i]));else{const l=Object.keys(e);o=new Array(l.length);for(let i=0,a=l.length;i_r(t)?!(t.type===Ye||t.type===we&&!Yl(t.children)):!0)?e:null}const so=e=>e?ii(e)?$r(e)||e.proxy:so(e.parent):null,Cn=Ae(Object.create(null),{$:e=>e,$el:e=>e.vnode.el,$data:e=>e.data,$props:e=>e.props,$attrs:e=>e.attrs,$slots:e=>e.slots,$refs:e=>e.refs,$parent:e=>so(e.parent),$root:e=>so(e.root),$emit:e=>e.emit,$options:e=>Oo(e),$forceUpdate:e=>e.f||(e.f=()=>Pr(e.update)),$nextTick:e=>e.n||(e.n=Sr.bind(e.proxy)),$watch:e=>fc.bind(e)}),zr=(e,t)=>e!==xe&&!e.__isScriptSetup&&de(e,t),Tc={get({_:e},t){const{ctx:n,setupState:r,data:o,props:s,accessCache:l,type:i,appContext:a}=e;let c;if(t[0]!=="$"){const g=l[t];if(g!==void 0)switch(g){case 1:return r[t];case 2:return o[t];case 4:return n[t];case 3:return s[t]}else{if(zr(r,t))return l[t]=1,r[t];if(o!==xe&&de(o,t))return l[t]=2,o[t];if((c=e.propsOptions[0])&&de(c,t))return l[t]=3,s[t];if(n!==xe&&de(n,t))return l[t]=4,n[t];lo&&(l[t]=0)}}const u=Cn[t];let f,h;if(u)return t==="$attrs"&&We(e,"get",t),u(e);if((f=i.__cssModules)&&(f=f[t]))return f;if(n!==xe&&de(n,t))return l[t]=4,n[t];if(h=a.config.globalProperties,de(h,t))return h[t]},set({_:e},t,n){const{data:r,setupState:o,ctx:s}=e;return zr(o,t)?(o[t]=n,!0):r!==xe&&de(r,t)?(r[t]=n,!0):de(e.props,t)||t[0]==="$"&&t.slice(1)in e?!1:(s[t]=n,!0)},has({_:{data:e,setupState:t,accessCache:n,ctx:r,appContext:o,propsOptions:s}},l){let i;return!!n[l]||e!==xe&&de(e,l)||zr(t,l)||(i=s[0])&&de(i,l)||de(r,l)||de(Cn,l)||de(o.config.globalProperties,l)},defineProperty(e,t,n){return n.get!=null?e._.accessCache[t]=0:de(n,"value")&&this.set(e,t,n.value,null),Reflect.defineProperty(e,t,n)}};function cs(e){return Q(e)?e.reduce((t,n)=>(t[n]=null,t),{}):e}let lo=!0;function Sc(e){const t=Oo(e),n=e.proxy,r=e.ctx;lo=!1,t.beforeCreate&&us(t.beforeCreate,e,"bc");const{data:o,computed:s,methods:l,watch:i,provide:a,inject:c,created:u,beforeMount:f,mounted:h,beforeUpdate:g,updated:y,activated:w,deactivated:C,beforeDestroy:v,beforeUnmount:b,destroyed:P,unmounted:S,render:U,renderTracked:Z,renderTriggered:$,errorCaptured:m,serverPrefetch:z,expose:M,inheritAttrs:G,components:L,directives:R,filters:I}=t;if(c&&Pc(c,r,null),l)for(const re in l){const se=l[re];oe(se)&&(r[re]=se.bind(n))}if(o){const re=o.call(n,n);ke(re)&&(e.data=vn(re))}if(lo=!0,s)for(const re in s){const se=s[re],He=oe(se)?se.bind(n,n):oe(se.get)?se.get.bind(n,n):ot,Ne=!oe(se)&&oe(se.set)?se.set.bind(n):ot,Ve=F({get:He,set:Ne});Object.defineProperty(r,re,{enumerable:!0,configurable:!0,get:()=>Ve.value,set:Fe=>Ve.value=Fe})}if(i)for(const re in i)Jl(i[re],r,n,re);if(a){const re=oe(a)?a.call(n):a;Reflect.ownKeys(re).forEach(se=>{Wt(se,re[se])})}u&&us(u,e,"c");function V(re,se){Q(se)?se.forEach(He=>re(He.bind(n))):se&&re(se.bind(n))}if(V(_c,f),V(Je,h),V(bc,g),V(yc,y),V(mc,w),V(gc,C),V(xc,m),V(kc,Z),V(wc,$),V(Un,b),V(Rr,S),V(Ec,z),Q(M))if(M.length){const re=e.exposed||(e.exposed={});M.forEach(se=>{Object.defineProperty(re,se,{get:()=>n[se],set:He=>n[se]=He})})}else e.exposed||(e.exposed={});U&&e.render===ot&&(e.render=U),G!=null&&(e.inheritAttrs=G),L&&(e.components=L),R&&(e.directives=R)}function Pc(e,t,n=ot){Q(e)&&(e=io(e));for(const r in e){const o=e[r];let s;ke(o)?"default"in o?s=Pe(o.from||r,o.default,!0):s=Pe(o.from||r):s=Pe(o),Ie(s)?Object.defineProperty(t,r,{enumerable:!0,configurable:!0,get:()=>s.value,set:l=>s.value=l}):t[r]=s}}function us(e,t,n){Xe(Q(e)?e.map(r=>r.bind(t.proxy)):e.bind(t.proxy),t,n)}function Jl(e,t,n,r){const o=r.includes(".")?Vl(n,r):()=>n[r];if(ge(e)){const s=t[e];oe(s)&&st(o,s)}else if(oe(e))st(o,e.bind(n));else if(ke(e))if(Q(e))e.forEach(s=>Jl(s,t,n,r));else{const s=oe(e.handler)?e.handler.bind(n):t[e.handler];oe(s)&&st(o,s,e)}}function Oo(e){const t=e.type,{mixins:n,extends:r}=t,{mixins:o,optionsCache:s,config:{optionMergeStrategies:l}}=e.appContext,i=s.get(t);let a;return i?a=i:!o.length&&!n&&!r?a=t:(a={},o.length&&o.forEach(c=>mr(a,c,l,!0)),mr(a,t,l)),ke(t)&&s.set(t,a),a}function mr(e,t,n,r=!1){const{mixins:o,extends:s}=t;s&&mr(e,s,n,!0),o&&o.forEach(l=>mr(e,l,n,!0));for(const l in t)if(!(r&&l==="expose")){const i=Ac[l]||n&&n[l];e[l]=i?i(e[l],t[l]):t[l]}return e}const Ac={data:fs,props:ds,emits:ds,methods:xn,computed:xn,beforeCreate:Be,created:Be,beforeMount:Be,mounted:Be,beforeUpdate:Be,updated:Be,beforeDestroy:Be,beforeUnmount:Be,destroyed:Be,unmounted:Be,activated:Be,deactivated:Be,errorCaptured:Be,serverPrefetch:Be,components:xn,directives:xn,watch:Rc,provide:fs,inject:Oc};function fs(e,t){return t?e?function(){return Ae(oe(e)?e.call(this,this):e,oe(t)?t.call(this,this):t)}:t:e}function Oc(e,t){return xn(io(e),io(t))}function io(e){if(Q(e)){const t={};for(let n=0;n1)return n&&oe(t)?t.call(r&&r.proxy):t}}function Nc(e,t,n,r=!1){const o={},s={};cr(s,Ir,1),e.propsDefaults=Object.create(null),Zl(e,t,o,s);for(const l in e.propsOptions[0])l in o||(o[l]=void 0);n?e.props=r?o:Va(o):e.type.props?e.props=o:e.props=s,e.attrs=s}function Dc(e,t,n,r){const{props:o,attrs:s,vnode:{patchFlag:l}}=e,i=pe(o),[a]=e.propsOptions;let c=!1;if((r||l>0)&&!(l&16)){if(l&8){const u=e.vnode.dynamicProps;for(let f=0;f{a=!0;const[h,g]=Xl(f,t,!0);Ae(l,h),g&&i.push(...g)};!n&&t.mixins.length&&t.mixins.forEach(u),e.extends&&u(e.extends),e.mixins&&e.mixins.forEach(u)}if(!s&&!a)return ke(e)&&r.set(e,rn),rn;if(Q(s))for(let u=0;u-1,g[1]=w<0||y-1||de(g,"default"))&&i.push(f)}}}const c=[l,i];return ke(e)&&r.set(e,c),c}function hs(e){return e[0]!=="$"}function ps(e){const t=e&&e.toString().match(/^\s*(function|class) (\w+)/);return t?t[2]:e===null?"null":""}function ms(e,t){return ps(e)===ps(t)}function gs(e,t){return Q(t)?t.findIndex(n=>ms(n,e)):oe(t)&&ms(t,e)?0:-1}const ei=e=>e[0]==="_"||e==="$stable",Ro=e=>Q(e)?e.map(tt):[tt(e)],Mc=(e,t,n)=>{if(t._n)return t;const r=De((...o)=>Ro(t(...o)),n);return r._c=!1,r},ti=(e,t,n)=>{const r=e._ctx;for(const o in e){if(ei(o))continue;const s=e[o];if(oe(s))t[o]=Mc(o,s,r);else if(s!=null){const l=Ro(s);t[o]=()=>l}}},ni=(e,t)=>{const n=Ro(t);e.slots.default=()=>n},Hc=(e,t)=>{if(e.vnode.shapeFlag&32){const n=t._;n?(e.slots=pe(t),cr(t,"_",n)):ti(t,e.slots={})}else e.slots={},t&&ni(e,t);cr(e.slots,Ir,1)},Fc=(e,t,n)=>{const{vnode:r,slots:o}=e;let s=!0,l=xe;if(r.shapeFlag&32){const i=t._;i?n&&i===1?s=!1:(Ae(o,t),!n&&i===1&&delete o._):(s=!t.$stable,ti(t,o)),l=t}else t&&(ni(e,t),l={default:1});if(s)for(const i in o)!ei(i)&&!(i in l)&&delete o[i]};function vr(e,t,n,r,o=!1){if(Q(e)){e.forEach((h,g)=>vr(h,t&&(Q(t)?t[g]:t),n,r,o));return}if(an(r)&&!o)return;const s=r.shapeFlag&4?$r(r.component)||r.component.proxy:r.el,l=o?null:s,{i,r:a}=e,c=t&&t.r,u=i.refs===xe?i.refs={}:i.refs,f=i.setupState;if(c!=null&&c!==a&&(ge(c)?(u[c]=null,de(f,c)&&(f[c]=null)):Ie(c)&&(c.value=null)),oe(a))Ot(a,i,12,[l,u]);else{const h=ge(a),g=Ie(a);if(h||g){const y=()=>{if(e.f){const w=h?de(f,a)?f[a]:u[a]:a.value;o?Q(w)&&vo(w,s):Q(w)?w.includes(s)||w.push(s):h?(u[a]=[s],de(f,a)&&(f[a]=u[a])):(a.value=[s],e.k&&(u[e.k]=a.value))}else h?(u[a]=l,de(f,a)&&(f[a]=l)):g&&(a.value=l,e.k&&(u[e.k]=l))};l?(y.id=-1,Ue(y,n)):y()}}}let xt=!1;const er=e=>/svg/.test(e.namespaceURI)&&e.tagName!=="foreignObject",tr=e=>e.nodeType===8;function jc(e){const{mt:t,p:n,o:{patchProp:r,createText:o,nextSibling:s,parentNode:l,remove:i,insert:a,createComment:c}}=e,u=(v,b)=>{if(!b.hasChildNodes()){n(null,v,b),dr(),b._vnode=v;return}xt=!1,f(b.firstChild,v,null,null,null),dr(),b._vnode=v,xt&&console.error("Hydration completed but contains mismatches.")},f=(v,b,P,S,U,Z=!1)=>{const $=tr(v)&&v.data==="[",m=()=>w(v,b,P,S,U,$),{type:z,ref:M,shapeFlag:G,patchFlag:L}=b;let R=v.nodeType;b.el=v,L===-2&&(Z=!1,b.dynamicChildren=null);let I=null;switch(z){case un:R!==3?b.children===""?(a(b.el=o(""),l(v),v),I=v):I=m():(v.data!==b.children&&(xt=!0,v.data=b.children),I=s(v));break;case Ye:R!==8||$?I=m():I=s(v);break;case Tn:if($&&(v=s(v),R=v.nodeType),R===1||R===3){I=v;const le=!b.children.length;for(let V=0;V{Z=Z||!!b.dynamicChildren;const{type:$,props:m,patchFlag:z,shapeFlag:M,dirs:G}=b,L=$==="input"&&G||$==="option";if(L||z!==-1){if(G&&ct(b,null,P,"created"),m)if(L||!Z||z&48)for(const I in m)(L&&I.endsWith("value")||Fn(I)&&!Ln(I))&&r(v,I,null,m[I],!1,void 0,P);else m.onClick&&r(v,"onClick",null,m.onClick,!1,void 0,P);let R;if((R=m&&m.onVnodeBeforeMount)&&Ze(R,P,b),G&&ct(b,null,P,"beforeMount"),((R=m&&m.onVnodeMounted)||G)&&zl(()=>{R&&Ze(R,P,b),G&&ct(b,null,P,"mounted")},S),M&16&&!(m&&(m.innerHTML||m.textContent))){let I=g(v.firstChild,b,v,P,S,U,Z);for(;I;){xt=!0;const le=I;I=I.nextSibling,i(le)}}else M&8&&v.textContent!==b.children&&(xt=!0,v.textContent=b.children)}return v.nextSibling},g=(v,b,P,S,U,Z,$)=>{$=$||!!b.dynamicChildren;const m=b.children,z=m.length;for(let M=0;M{const{slotScopeIds:$}=b;$&&(U=U?U.concat($):$);const m=l(v),z=g(s(v),b,m,P,S,U,Z);return z&&tr(z)&&z.data==="]"?s(b.anchor=z):(xt=!0,a(b.anchor=c("]"),m,z),z)},w=(v,b,P,S,U,Z)=>{if(xt=!0,b.el=null,Z){const z=C(v);for(;;){const M=s(v);if(M&&M!==z)i(M);else break}}const $=s(v),m=l(v);return i(v),n(null,b,m,$,P,S,er(m),U),$},C=v=>{let b=0;for(;v;)if(v=s(v),v&&tr(v)&&(v.data==="["&&b++,v.data==="]")){if(b===0)return s(v);b--}return v};return[u,f]}const Ue=zl;function Bc(e){return zc(e,jc)}function zc(e,t){const n=Zr();n.__VUE__=!0;const{insert:r,remove:o,patchProp:s,createElement:l,createText:i,createComment:a,setText:c,setElementText:u,parentNode:f,nextSibling:h,setScopeId:g=ot,insertStaticContent:y}=e,w=(d,p,_,E=null,x=null,T=null,H=!1,O=null,D=!!p.dynamicChildren)=>{if(d===p)return;d&&!zt(d,p)&&(E=k(d),Fe(d,x,T,!0),d=null),p.patchFlag===-2&&(D=!1,p.dynamicChildren=null);const{type:A,ref:Y,shapeFlag:q}=p;switch(A){case un:C(d,p,_,E);break;case Ye:v(d,p,_,E);break;case Tn:d==null&&b(p,_,E,H);break;case we:L(d,p,_,E,x,T,H,O,D);break;default:q&1?U(d,p,_,E,x,T,H,O,D):q&6?R(d,p,_,E,x,T,H,O,D):(q&64||q&128)&&A.process(d,p,_,E,x,T,H,O,D,N)}Y!=null&&x&&vr(Y,d&&d.ref,T,p||d,!p)},C=(d,p,_,E)=>{if(d==null)r(p.el=i(p.children),_,E);else{const x=p.el=d.el;p.children!==d.children&&c(x,p.children)}},v=(d,p,_,E)=>{d==null?r(p.el=a(p.children||""),_,E):p.el=d.el},b=(d,p,_,E)=>{[d.el,d.anchor]=y(d.children,p,_,E,d.el,d.anchor)},P=({el:d,anchor:p},_,E)=>{let x;for(;d&&d!==p;)x=h(d),r(d,_,E),d=x;r(p,_,E)},S=({el:d,anchor:p})=>{let _;for(;d&&d!==p;)_=h(d),o(d),d=_;o(p)},U=(d,p,_,E,x,T,H,O,D)=>{H=H||p.type==="svg",d==null?Z(p,_,E,x,T,H,O,D):z(d,p,x,T,H,O,D)},Z=(d,p,_,E,x,T,H,O)=>{let D,A;const{type:Y,props:q,shapeFlag:J,transition:ne,dirs:ie}=d;if(D=d.el=l(d.type,T,q&&q.is,q),J&8?u(D,d.children):J&16&&m(d.children,D,null,E,x,T&&Y!=="foreignObject",H,O),ie&&ct(d,null,E,"created"),$(D,d,d.scopeId,H,E),q){for(const be in q)be!=="value"&&!Ln(be)&&s(D,be,null,q[be],T,d.children,E,x,$e);"value"in q&&s(D,"value",null,q.value),(A=q.onVnodeBeforeMount)&&Ze(A,E,d)}ie&&ct(d,null,E,"beforeMount");const ye=(!x||x&&!x.pendingBranch)&&ne&&!ne.persisted;ye&&ne.beforeEnter(D),r(D,p,_),((A=q&&q.onVnodeMounted)||ye||ie)&&Ue(()=>{A&&Ze(A,E,d),ye&&ne.enter(D),ie&&ct(d,null,E,"mounted")},x)},$=(d,p,_,E,x)=>{if(_&&g(d,_),E)for(let T=0;T{for(let A=D;A{const O=p.el=d.el;let{patchFlag:D,dynamicChildren:A,dirs:Y}=p;D|=d.patchFlag&16;const q=d.props||xe,J=p.props||xe;let ne;_&&Mt(_,!1),(ne=J.onVnodeBeforeUpdate)&&Ze(ne,_,p,d),Y&&ct(p,d,_,"beforeUpdate"),_&&Mt(_,!0);const ie=x&&p.type!=="foreignObject";if(A?M(d.dynamicChildren,A,O,_,E,ie,T):H||se(d,p,O,null,_,E,ie,T,!1),D>0){if(D&16)G(O,p,q,J,_,E,x);else if(D&2&&q.class!==J.class&&s(O,"class",null,J.class,x),D&4&&s(O,"style",q.style,J.style,x),D&8){const ye=p.dynamicProps;for(let be=0;be{ne&&Ze(ne,_,p,d),Y&&ct(p,d,_,"updated")},E)},M=(d,p,_,E,x,T,H)=>{for(let O=0;O{if(_!==E){if(_!==xe)for(const O in _)!Ln(O)&&!(O in E)&&s(d,O,_[O],null,H,p.children,x,T,$e);for(const O in E){if(Ln(O))continue;const D=E[O],A=_[O];D!==A&&O!=="value"&&s(d,O,A,D,H,p.children,x,T,$e)}"value"in E&&s(d,"value",_.value,E.value)}},L=(d,p,_,E,x,T,H,O,D)=>{const A=p.el=d?d.el:i(""),Y=p.anchor=d?d.anchor:i("");let{patchFlag:q,dynamicChildren:J,slotScopeIds:ne}=p;ne&&(O=O?O.concat(ne):ne),d==null?(r(A,_,E),r(Y,_,E),m(p.children,_,Y,x,T,H,O,D)):q>0&&q&64&&J&&d.dynamicChildren?(M(d.dynamicChildren,J,_,x,T,H,O),(p.key!=null||x&&p===x.subTree)&&ri(d,p,!0)):se(d,p,_,Y,x,T,H,O,D)},R=(d,p,_,E,x,T,H,O,D)=>{p.slotScopeIds=O,d==null?p.shapeFlag&512?x.ctx.activate(p,_,E,H,D):I(p,_,E,x,T,H,D):le(d,p,D)},I=(d,p,_,E,x,T,H)=>{const O=d.component=Qc(d,E,x);if(Vn(d)&&(O.ctx.renderer=N),Zc(O),O.asyncDep){if(x&&x.registerDep(O,V),!d.el){const D=O.subTree=te(Ye);v(null,D,p,_)}return}V(O,d,p,_,x,T,H)},le=(d,p,_)=>{const E=p.component=d.component;if(ic(d,p,_))if(E.asyncDep&&!E.asyncResolved){re(E,p,_);return}else E.next=p,tc(E.update),E.update();else p.el=d.el,E.vnode=p},V=(d,p,_,E,x,T,H)=>{const O=()=>{if(d.isMounted){let{next:Y,bu:q,u:J,parent:ne,vnode:ie}=d,ye=Y,be;Mt(d,!1),Y?(Y.el=ie.el,re(d,Y,H)):Y=ie,q&&Hr(q),(be=Y.props&&Y.props.onVnodeBeforeUpdate)&&Ze(be,ne,Y,ie),Mt(d,!0);const Te=Fr(d),et=d.subTree;d.subTree=Te,w(et,Te,f(et.el),k(et),d,x,T),Y.el=Te.el,ye===null&&ac(d,Te.el),J&&Ue(J,x),(be=Y.props&&Y.props.onVnodeUpdated)&&Ue(()=>Ze(be,ne,Y,ie),x)}else{let Y;const{el:q,props:J}=p,{bm:ne,m:ie,parent:ye}=d,be=an(p);if(Mt(d,!1),ne&&Hr(ne),!be&&(Y=J&&J.onVnodeBeforeMount)&&Ze(Y,ye,p),Mt(d,!0),q&&ce){const Te=()=>{d.subTree=Fr(d),ce(q,d.subTree,d,x,null)};be?p.type.__asyncLoader().then(()=>!d.isUnmounted&&Te()):Te()}else{const Te=d.subTree=Fr(d);w(null,Te,_,E,d,x,T),p.el=Te.el}if(ie&&Ue(ie,x),!be&&(Y=J&&J.onVnodeMounted)){const Te=p;Ue(()=>Ze(Y,ye,Te),x)}(p.shapeFlag&256||ye&&an(ye.vnode)&&ye.vnode.shapeFlag&256)&&d.a&&Ue(d.a,x),d.isMounted=!0,p=_=E=null}},D=d.effect=new Eo(O,()=>Pr(A),d.scope),A=d.update=()=>D.run();A.id=d.uid,Mt(d,!0),A()},re=(d,p,_)=>{p.component=d;const E=d.vnode.props;d.vnode=p,d.next=null,Dc(d,p.props,E,_),Fc(d,p.children,_),mn(),ss(),gn()},se=(d,p,_,E,x,T,H,O,D=!1)=>{const A=d&&d.children,Y=d?d.shapeFlag:0,q=p.children,{patchFlag:J,shapeFlag:ne}=p;if(J>0){if(J&128){Ne(A,q,_,E,x,T,H,O,D);return}else if(J&256){He(A,q,_,E,x,T,H,O,D);return}}ne&8?(Y&16&&$e(A,x,T),q!==A&&u(_,q)):Y&16?ne&16?Ne(A,q,_,E,x,T,H,O,D):$e(A,x,T,!0):(Y&8&&u(_,""),ne&16&&m(q,_,E,x,T,H,O,D))},He=(d,p,_,E,x,T,H,O,D)=>{d=d||rn,p=p||rn;const A=d.length,Y=p.length,q=Math.min(A,Y);let J;for(J=0;JY?$e(d,x,T,!0,!1,q):m(p,_,E,x,T,H,O,D,q)},Ne=(d,p,_,E,x,T,H,O,D)=>{let A=0;const Y=p.length;let q=d.length-1,J=Y-1;for(;A<=q&&A<=J;){const ne=d[A],ie=p[A]=D?Tt(p[A]):tt(p[A]);if(zt(ne,ie))w(ne,ie,_,null,x,T,H,O,D);else break;A++}for(;A<=q&&A<=J;){const ne=d[q],ie=p[J]=D?Tt(p[J]):tt(p[J]);if(zt(ne,ie))w(ne,ie,_,null,x,T,H,O,D);else break;q--,J--}if(A>q){if(A<=J){const ne=J+1,ie=neJ)for(;A<=q;)Fe(d[A],x,T,!0),A++;else{const ne=A,ie=A,ye=new Map;for(A=ie;A<=J;A++){const Ke=p[A]=D?Tt(p[A]):tt(p[A]);Ke.key!=null&&ye.set(Ke.key,A)}let be,Te=0;const et=J-ie+1;let Zt=!1,Ko=0;const _n=new Array(et);for(A=0;A=et){Fe(Ke,x,T,!0);continue}let at;if(Ke.key!=null)at=ye.get(Ke.key);else for(be=ie;be<=J;be++)if(_n[be-ie]===0&&zt(Ke,p[be])){at=be;break}at===void 0?Fe(Ke,x,T,!0):(_n[at-ie]=A+1,at>=Ko?Ko=at:Zt=!0,w(Ke,p[at],_,null,x,T,H,O,D),Te++)}const Go=Zt?Vc(_n):rn;for(be=Go.length-1,A=et-1;A>=0;A--){const Ke=ie+A,at=p[Ke],Yo=Ke+1{const{el:T,type:H,transition:O,children:D,shapeFlag:A}=d;if(A&6){Ve(d.component.subTree,p,_,E);return}if(A&128){d.suspense.move(p,_,E);return}if(A&64){H.move(d,p,_,N);return}if(H===we){r(T,p,_);for(let q=0;qO.enter(T),x);else{const{leave:q,delayLeave:J,afterLeave:ne}=O,ie=()=>r(T,p,_),ye=()=>{q(T,()=>{ie(),ne&&ne()})};J?J(T,ie,ye):ye()}else r(T,p,_)},Fe=(d,p,_,E=!1,x=!1)=>{const{type:T,props:H,ref:O,children:D,dynamicChildren:A,shapeFlag:Y,patchFlag:q,dirs:J}=d;if(O!=null&&vr(O,null,_,d,!0),Y&256){p.ctx.deactivate(d);return}const ne=Y&1&&J,ie=!an(d);let ye;if(ie&&(ye=H&&H.onVnodeBeforeUnmount)&&Ze(ye,p,d),Y&6)it(d.component,_,E);else{if(Y&128){d.suspense.unmount(_,E);return}ne&&ct(d,null,p,"beforeUnmount"),Y&64?d.type.remove(d,p,_,x,N,E):A&&(T!==we||q>0&&q&64)?$e(A,p,_,!1,!0):(T===we&&q&384||!x&&Y&16)&&$e(D,p,_),E&&Et(d)}(ie&&(ye=H&&H.onVnodeUnmounted)||ne)&&Ue(()=>{ye&&Ze(ye,p,d),ne&&ct(d,null,p,"unmounted")},_)},Et=d=>{const{type:p,el:_,anchor:E,transition:x}=d;if(p===we){wt(_,E);return}if(p===Tn){S(d);return}const T=()=>{o(_),x&&!x.persisted&&x.afterLeave&&x.afterLeave()};if(d.shapeFlag&1&&x&&!x.persisted){const{leave:H,delayLeave:O}=x,D=()=>H(_,T);O?O(d.el,T,D):D()}else T()},wt=(d,p)=>{let _;for(;d!==p;)_=h(d),o(d),d=_;o(p)},it=(d,p,_)=>{const{bum:E,scope:x,update:T,subTree:H,um:O}=d;E&&Hr(E),x.stop(),T&&(T.active=!1,Fe(H,d,p,_)),O&&Ue(O,p),Ue(()=>{d.isUnmounted=!0},p),p&&p.pendingBranch&&!p.isUnmounted&&d.asyncDep&&!d.asyncResolved&&d.suspenseId===p.pendingId&&(p.deps--,p.deps===0&&p.resolve())},$e=(d,p,_,E=!1,x=!1,T=0)=>{for(let H=T;Hd.shapeFlag&6?k(d.component.subTree):d.shapeFlag&128?d.suspense.next():h(d.anchor||d.el),B=(d,p,_)=>{d==null?p._vnode&&Fe(p._vnode,null,null,!0):w(p._vnode||null,d,p,null,null,null,_),ss(),dr(),p._vnode=d},N={p:w,um:Fe,m:Ve,r:Et,mt:I,mc:m,pc:se,pbc:M,n:k,o:e};let K,ce;return t&&([K,ce]=t(N)),{render:B,hydrate:K,createApp:$c(B,K)}}function Mt({effect:e,update:t},n){e.allowRecurse=t.allowRecurse=n}function ri(e,t,n=!1){const r=e.children,o=t.children;if(Q(r)&&Q(o))for(let s=0;s>1,e[n[i]]0&&(t[r]=n[s-1]),n[s]=r)}}for(s=n.length,l=n[s-1];s-- >0;)n[s]=l,l=t[l];return n}const Uc=e=>e.__isTeleport,we=Symbol.for("v-fgt"),un=Symbol.for("v-txt"),Ye=Symbol.for("v-cmt"),Tn=Symbol.for("v-stc"),Sn=[];let rt=null;function j(e=!1){Sn.push(rt=e?null:[])}function qc(){Sn.pop(),rt=Sn[Sn.length-1]||null}let Nn=1;function vs(e){Nn+=e}function oi(e){return e.dynamicChildren=Nn>0?rt||rn:null,qc(),Nn>0&&rt&&rt.push(e),e}function X(e,t,n,r,o,s){return oi(he(e,t,n,r,o,s,!0))}function Se(e,t,n,r,o){return oi(te(e,t,n,r,o,!0))}function _r(e){return e?e.__v_isVNode===!0:!1}function zt(e,t){return e.type===t.type&&e.key===t.key}const Ir="__vInternal",si=({key:e})=>e??null,ir=({ref:e,ref_key:t,ref_for:n})=>(typeof e=="number"&&(e=""+e),e!=null?ge(e)||Ie(e)||oe(e)?{i:Me,r:e,k:t,f:!!n}:e:null);function he(e,t=null,n=null,r=0,o=null,s=e===we?0:1,l=!1,i=!1){const a={__v_isVNode:!0,__v_skip:!0,type:e,props:t,key:t&&si(t),ref:t&&ir(t),scopeId:Bl,slotScopeIds:null,children:n,component:null,suspense:null,ssContent:null,ssFallback:null,dirs:null,transition:null,el:null,anchor:null,target:null,targetAnchor:null,staticCount:0,shapeFlag:s,patchFlag:r,dynamicProps:o,dynamicChildren:null,appContext:null,ctx:Me};return i?(Io(a,n),s&128&&e.normalize(a)):n&&(a.shapeFlag|=ge(n)?8:16),Nn>0&&!l&&rt&&(a.patchFlag>0||s&6)&&a.patchFlag!==32&&rt.push(a),a}const te=Wc;function Wc(e,t=null,n=null,r=0,o=null,s=!1){if((!e||e===Lc)&&(e=Ye),_r(e)){const i=$t(e,t,!0);return n&&Io(i,n),Nn>0&&!s&&rt&&(i.shapeFlag&6?rt[rt.indexOf(e)]=i:rt.push(i)),i.patchFlag|=-2,i}if(ru(e)&&(e=e.__vccOpts),t){t=Kc(t);let{class:i,style:a}=t;i&&!ge(i)&&(t.class=qe(i)),ke(a)&&(Ol(a)&&!Q(a)&&(a=Ae({},a)),t.style=jn(a))}const l=ge(e)?1:cc(e)?128:Uc(e)?64:ke(e)?4:oe(e)?2:0;return he(e,t,n,r,o,l,s,!0)}function Kc(e){return e?Ol(e)||Ir in e?Ae({},e):e:null}function $t(e,t,n=!1){const{props:r,ref:o,patchFlag:s,children:l}=e,i=t?co(r||{},t):r;return{__v_isVNode:!0,__v_skip:!0,type:e.type,props:i,key:i&&si(i),ref:t&&t.ref?n&&o?Q(o)?o.concat(ir(t)):[o,ir(t)]:ir(t):o,scopeId:e.scopeId,slotScopeIds:e.slotScopeIds,children:l,target:e.target,targetAnchor:e.targetAnchor,staticCount:e.staticCount,shapeFlag:e.shapeFlag,patchFlag:t&&e.type!==we?s===-1?16:s|16:s,dynamicProps:e.dynamicProps,dynamicChildren:e.dynamicChildren,appContext:e.appContext,dirs:e.dirs,transition:e.transition,component:e.component,suspense:e.suspense,ssContent:e.ssContent&&$t(e.ssContent),ssFallback:e.ssFallback&&$t(e.ssFallback),el:e.el,anchor:e.anchor,ctx:e.ctx,ce:e.ce}}function Nt(e=" ",t=0){return te(un,null,e,t)}function Gc(e,t){const n=te(Tn,null,e);return n.staticCount=t,n}function Ce(e="",t=!1){return t?(j(),Se(Ye,null,e)):te(Ye,null,e)}function tt(e){return e==null||typeof e=="boolean"?te(Ye):Q(e)?te(we,null,e.slice()):typeof e=="object"?Tt(e):te(un,null,String(e))}function Tt(e){return e.el===null&&e.patchFlag!==-1||e.memo?e:$t(e)}function Io(e,t){let n=0;const{shapeFlag:r}=e;if(t==null)t=null;else if(Q(t))n=16;else if(typeof t=="object")if(r&65){const o=t.default;o&&(o._c&&(o._d=!1),Io(e,o()),o._c&&(o._d=!0));return}else{n=32;const o=t._;!o&&!(Ir in t)?t._ctx=Me:o===3&&Me&&(Me.slots._===1?t._=1:(t._=2,e.patchFlag|=1024))}else oe(t)?(t={default:t,_ctx:Me},n=32):(t=String(t),r&64?(n=16,t=[Nt(t)]):n=8);e.children=t,e.shapeFlag|=n}function co(...e){const t={};for(let n=0;nOe||Me;let $o,Xt,_s="__VUE_INSTANCE_SETTERS__";(Xt=Zr()[_s])||(Xt=Zr()[_s]=[]),Xt.push(e=>Oe=e),$o=e=>{Xt.length>1?Xt.forEach(t=>t(e)):Xt[0](e)};const fn=e=>{$o(e),e.scope.on()},Kt=()=>{Oe&&Oe.scope.off(),$o(null)};function ii(e){return e.vnode.shapeFlag&4}let dn=!1;function Zc(e,t=!1){dn=t;const{props:n,children:r}=e.vnode,o=ii(e);Nc(e,n,o,t),Hc(e,r);const s=o?Xc(e,t):void 0;return dn=!1,s}function Xc(e,t){const n=e.type;e.accessCache=Object.create(null),e.proxy=Rl(new Proxy(e.ctx,Tc));const{setup:r}=n;if(r){const o=e.setupContext=r.length>1?tu(e):null;fn(e),mn();const s=Ot(r,e,0,[e.props,o]);if(gn(),Kt(),ml(s)){if(s.then(Kt,Kt),t)return s.then(l=>{bs(e,l,t)}).catch(l=>{zn(l,e,0)});e.asyncDep=s}else bs(e,s,t)}else ai(e,t)}function bs(e,t,n){oe(t)?e.type.__ssrInlineRender?e.ssrRender=t:e.render=t:ke(t)&&(e.setupState=Nl(t)),ai(e,n)}let ys;function ai(e,t,n){const r=e.type;if(!e.render){if(!t&&ys&&!r.render){const o=r.template||Oo(e).template;if(o){const{isCustomElement:s,compilerOptions:l}=e.appContext.config,{delimiters:i,compilerOptions:a}=r,c=Ae(Ae({isCustomElement:s,delimiters:i},l),a);r.render=ys(o,c)}}e.render=r.render||ot}fn(e),mn(),Sc(e),gn(),Kt()}function eu(e){return e.attrsProxy||(e.attrsProxy=new Proxy(e.attrs,{get(t,n){return We(e,"get","$attrs"),t[n]}}))}function tu(e){const t=n=>{e.exposed=n||{}};return{get attrs(){return eu(e)},slots:e.slots,emit:e.emit,expose:t}}function $r(e){if(e.exposed)return e.exposeProxy||(e.exposeProxy=new Proxy(Nl(Rl(e.exposed)),{get(t,n){if(n in t)return t[n];if(n in Cn)return Cn[n](e)},has(t,n){return n in t||n in Cn}}))}function nu(e,t=!0){return oe(e)?e.displayName||e.name:e.name||t&&e.__name}function ru(e){return oe(e)&&"__vccOpts"in e}const F=(e,t)=>Za(e,t,dn);function ae(e,t,n){const r=arguments.length;return r===2?ke(t)&&!Q(t)?_r(t)?te(e,null,[t]):te(e,t):te(e,null,t):(r>3?n=Array.prototype.slice.call(arguments,2):r===3&&_r(n)&&(n=[n]),te(e,t,n))}const ou=Symbol.for("v-scx"),su=()=>Pe(ou),lu="3.3.4",iu="http://www.w3.org/2000/svg",Vt=typeof document<"u"?document:null,Es=Vt&&Vt.createElement("template"),au={insert:(e,t,n)=>{t.insertBefore(e,n||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,n,r)=>{const o=t?Vt.createElementNS(iu,e):Vt.createElement(e,n?{is:n}:void 0);return e==="select"&&r&&r.multiple!=null&&o.setAttribute("multiple",r.multiple),o},createText:e=>Vt.createTextNode(e),createComment:e=>Vt.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>Vt.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,n,r,o,s){const l=n?n.previousSibling:t.lastChild;if(o&&(o===s||o.nextSibling))for(;t.insertBefore(o.cloneNode(!0),n),!(o===s||!(o=o.nextSibling)););else{Es.innerHTML=r?`${e}`:e;const i=Es.content;if(r){const a=i.firstChild;for(;a.firstChild;)i.appendChild(a.firstChild);i.removeChild(a)}t.insertBefore(i,n)}return[l?l.nextSibling:t.firstChild,n?n.previousSibling:t.lastChild]}};function cu(e,t,n){const r=e._vtc;r&&(t=(t?[t,...r]:[...r]).join(" ")),t==null?e.removeAttribute("class"):n?e.setAttribute("class",t):e.className=t}function uu(e,t,n){const r=e.style,o=ge(n);if(n&&!o){if(t&&!ge(t))for(const s in t)n[s]==null&&uo(r,s,"");for(const s in n)uo(r,s,n[s])}else{const s=r.display;o?t!==n&&(r.cssText=n):t&&e.removeAttribute("style"),"_vod"in e&&(r.display=s)}}const ws=/\s*!important$/;function uo(e,t,n){if(Q(n))n.forEach(r=>uo(e,t,r));else if(n==null&&(n=""),t.startsWith("--"))e.setProperty(t,n);else{const r=fu(e,t);ws.test(n)?e.setProperty(Yt(r),n.replace(ws,""),"important"):e[r]=n}}const ks=["Webkit","Moz","ms"],Vr={};function fu(e,t){const n=Vr[t];if(n)return n;let r=ft(t);if(r!=="filter"&&r in e)return Vr[t]=r;r=Lr(r);for(let o=0;oUr||(_u.then(()=>Ur=0),Ur=Date.now());function yu(e,t){const n=r=>{if(!r._vts)r._vts=Date.now();else if(r._vts<=n.attached)return;Xe(Eu(r,n.value),t,5,[r])};return n.value=e,n.attached=bu(),n}function Eu(e,t){if(Q(t)){const n=e.stopImmediatePropagation;return e.stopImmediatePropagation=()=>{n.call(e),e._stopped=!0},t.map(r=>o=>!o._stopped&&r&&r(o))}else return t}const Cs=/^on[a-z]/,wu=(e,t,n,r,o=!1,s,l,i,a)=>{t==="class"?cu(e,r,o):t==="style"?uu(e,n,r):Fn(t)?go(t)||gu(e,t,n,r,l):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):ku(e,t,r,o))?hu(e,t,r,s,l,i,a):(t==="true-value"?e._trueValue=r:t==="false-value"&&(e._falseValue=r),du(e,t,r,o))};function ku(e,t,n,r){return r?!!(t==="innerHTML"||t==="textContent"||t in e&&Cs.test(t)&&oe(n)):t==="spellcheck"||t==="draggable"||t==="translate"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA"||Cs.test(t)&&ge(n)?!1:t in e}const Lt="transition",bn="animation",qn=(e,{slots:t})=>ae(pc,xu(e),t);qn.displayName="Transition";const ci={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String};qn.props=Ae({},Ul,ci);const Ht=(e,t=[])=>{Q(e)?e.forEach(n=>n(...t)):e&&e(...t)},Ts=e=>e?Q(e)?e.some(t=>t.length>1):e.length>1:!1;function xu(e){const t={};for(const L in e)L in ci||(t[L]=e[L]);if(e.css===!1)return t;const{name:n="v",type:r,duration:o,enterFromClass:s=`${n}-enter-from`,enterActiveClass:l=`${n}-enter-active`,enterToClass:i=`${n}-enter-to`,appearFromClass:a=s,appearActiveClass:c=l,appearToClass:u=i,leaveFromClass:f=`${n}-leave-from`,leaveActiveClass:h=`${n}-leave-active`,leaveToClass:g=`${n}-leave-to`}=e,y=Lu(o),w=y&&y[0],C=y&&y[1],{onBeforeEnter:v,onEnter:b,onEnterCancelled:P,onLeave:S,onLeaveCancelled:U,onBeforeAppear:Z=v,onAppear:$=b,onAppearCancelled:m=P}=t,z=(L,R,I)=>{Ft(L,R?u:i),Ft(L,R?c:l),I&&I()},M=(L,R)=>{L._isLeaving=!1,Ft(L,f),Ft(L,g),Ft(L,h),R&&R()},G=L=>(R,I)=>{const le=L?$:b,V=()=>z(R,L,I);Ht(le,[R,V]),Ss(()=>{Ft(R,L?a:s),Ct(R,L?u:i),Ts(le)||Ps(R,r,w,V)})};return Ae(t,{onBeforeEnter(L){Ht(v,[L]),Ct(L,s),Ct(L,l)},onBeforeAppear(L){Ht(Z,[L]),Ct(L,a),Ct(L,c)},onEnter:G(!1),onAppear:G(!0),onLeave(L,R){L._isLeaving=!0;const I=()=>M(L,R);Ct(L,f),Su(),Ct(L,h),Ss(()=>{L._isLeaving&&(Ft(L,f),Ct(L,g),Ts(S)||Ps(L,r,C,I))}),Ht(S,[L,I])},onEnterCancelled(L){z(L,!1),Ht(P,[L])},onAppearCancelled(L){z(L,!0),Ht(m,[L])},onLeaveCancelled(L){M(L),Ht(U,[L])}})}function Lu(e){if(e==null)return null;if(ke(e))return[qr(e.enter),qr(e.leave)];{const t=qr(e);return[t,t]}}function qr(e){return la(e)}function Ct(e,t){t.split(/\s+/).forEach(n=>n&&e.classList.add(n)),(e._vtc||(e._vtc=new Set)).add(t)}function Ft(e,t){t.split(/\s+/).forEach(r=>r&&e.classList.remove(r));const{_vtc:n}=e;n&&(n.delete(t),n.size||(e._vtc=void 0))}function Ss(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let Cu=0;function Ps(e,t,n,r){const o=e._endId=++Cu,s=()=>{o===e._endId&&r()};if(n)return setTimeout(s,n);const{type:l,timeout:i,propCount:a}=Tu(e,t);if(!l)return r();const c=l+"end";let u=0;const f=()=>{e.removeEventListener(c,h),s()},h=g=>{g.target===e&&++u>=a&&f()};setTimeout(()=>{u(n[y]||"").split(", "),o=r(`${Lt}Delay`),s=r(`${Lt}Duration`),l=As(o,s),i=r(`${bn}Delay`),a=r(`${bn}Duration`),c=As(i,a);let u=null,f=0,h=0;t===Lt?l>0&&(u=Lt,f=l,h=s.length):t===bn?c>0&&(u=bn,f=c,h=a.length):(f=Math.max(l,c),u=f>0?l>c?Lt:bn:null,h=u?u===Lt?s.length:a.length:0);const g=u===Lt&&/\b(transform|all)(,|$)/.test(r(`${Lt}Property`).toString());return{type:u,timeout:f,propCount:h,hasTransform:g}}function As(e,t){for(;e.lengthOs(n)+Os(e[r])))}function Os(e){return Number(e.slice(0,-1).replace(",","."))*1e3}function Su(){return document.body.offsetHeight}const Pu={esc:"escape",space:" ",up:"arrow-up",left:"arrow-left",right:"arrow-right",down:"arrow-down",delete:"backspace"},Au=(e,t)=>n=>{if(!("key"in n))return;const r=Yt(n.key);if(t.some(o=>o===r||Pu[o]===r))return e(n)},br={beforeMount(e,{value:t},{transition:n}){e._vod=e.style.display==="none"?"":e.style.display,n&&t?n.beforeEnter(e):yn(e,t)},mounted(e,{value:t},{transition:n}){n&&t&&n.enter(e)},updated(e,{value:t,oldValue:n},{transition:r}){!t!=!n&&(r?t?(r.beforeEnter(e),yn(e,!0),r.enter(e)):r.leave(e,()=>{yn(e,!1)}):yn(e,t))},beforeUnmount(e,{value:t}){yn(e,t)}};function yn(e,t){e.style.display=t?e._vod:"none"}const Ou=Ae({patchProp:wu},au);let Wr,Rs=!1;function Ru(){return Wr=Rs?Wr:Bc(Ou),Rs=!0,Wr}const Iu=(...e)=>{const t=Ru().createApp(...e),{mount:n}=t;return t.mount=r=>{const o=$u(r);if(o)return n(o,!0,o instanceof SVGElement)},t};function $u(e){return ge(e)?document.querySelector(e):e}const Nu={"v-fe360bd6":()=>W(()=>import("./develop.html-117f0576.js"),[]).then(({data:e})=>e),"v-d08d435a":()=>W(()=>import("./introduce.html-6487c4a7.js"),[]).then(({data:e})=>e),"v-8daa1a0e":()=>W(()=>import("./index.html-c4c51566.js"),[]).then(({data:e})=>e),"v-7cbb39b9":()=>W(()=>import("./api_case.html-ba462121.js"),[]).then(({data:e})=>e),"v-109501ec":()=>W(()=>import("./api_object.html-2747dbac.js"),[]).then(({data:e})=>e),"v-d24a86f0":()=>W(()=>import("./assert.html-657c6f15.js"),[]).then(({data:e})=>e),"v-5b62ef19":()=>W(()=>import("./more.html-cc1b2581.js"),[]).then(({data:e})=>e),"v-23f9483c":()=>W(()=>import("./start.html-b31eab27.js"),[]).then(({data:e})=>e),"v-488b8cec":()=>W(()=>import("./webscocket.html-50b19ef9.js"),[]).then(({data:e})=>e),"v-53c50d87":()=>W(()=>import("./appium_lab.html-af1255b2.js"),[]).then(({data:e})=>e),"v-483ce5fe":()=>W(()=>import("./extensions.html-2fc302d1.js"),[]).then(({data:e})=>e),"v-299549e4":()=>W(()=>import("./page_object.html-cb039342.js"),[]).then(({data:e})=>e),"v-4370387b":()=>W(()=>import("./start.html-ea9f18a1.js"),[]).then(({data:e})=>e),"v-c350a662":()=>W(()=>import("./db_operation.html-55b16cfc.js"),[]).then(({data:e})=>e),"v-1d715ea7":()=>W(()=>import("./test_library.html-f4b42a84.js"),[]).then(({data:e})=>e),"v-d8f79a72":()=>W(()=>import("./advanced.html-c039b7bf.js"),[]).then(({data:e})=>e),"v-8e520eda":()=>W(()=>import("./create_project.html-3536f4e7.js"),[]).then(({data:e})=>e),"v-78c619cc":()=>W(()=>import("./data_driver.html-7c198d0a.js"),[]).then(({data:e})=>e),"v-4e6f0425":()=>W(()=>import("./dependent_func.html-f699bc84.js"),[]).then(({data:e})=>e),"v-4e8563af":()=>W(()=>import("./installation.html-02312a74.js"),[]).then(({data:e})=>e),"v-0f898c79":()=>W(()=>import("./quick_start.html-87f1da62.js"),[]).then(({data:e})=>e),"v-6f32df80":()=>W(()=>import("./seldom_cli.html-fcf52dd5.js"),[]).then(({data:e})=>e),"v-3cdc5c3a":()=>W(()=>import("./platform.html-f7529303.js"),[]).then(({data:e})=>e),"v-129a7066":()=>W(()=>import("./CHANGES.html-65323456.js"),[]).then(({data:e})=>e),"v-471218ee":()=>W(()=>import("./browser_driver.html-3b18ca47.js"),[]).then(({data:e})=>e),"v-198befa7":()=>W(()=>import("./chaining.html-c3eac4c8.js"),[]).then(({data:e})=>e),"v-1f2c830c":()=>W(()=>import("./other.html-5da407eb.js"),[]).then(({data:e})=>e),"v-0ccdb93b":()=>W(()=>import("./page_object.html-a01824ef.js"),[]).then(({data:e})=>e),"v-40ed12b6":()=>W(()=>import("./seldom_api.html-c0b2694d.js"),[]).then(({data:e})=>e),"v-3706649a":()=>W(()=>import("./404.html-60b35caa.js"),[]).then(({data:e})=>e)},Du=JSON.parse('{"base":"/","lang":"en-US","title":"seldom文档","description":"seldom 是基于unittest 的自动化测试框架。","head":[["link",{"rel":"icon","href":"/logo.jpeg"}]],"locales":{}}');var Mu=([e,t,n])=>e==="meta"&&t.name?`${e}.${t.name}`:["title","base"].includes(e)?e:e==="template"&&t.id?`${e}.${t.id}`:JSON.stringify([e,t,n]),Hu=e=>{const t=new Set,n=[];return e.forEach(r=>{const o=Mu(r);t.has(o)||(t.add(o),n.push(r))}),n},Wn=e=>/^(https?:)?\/\//.test(e),Fu=e=>/^mailto:/.test(e),ju=e=>/^tel:/.test(e),No=e=>Object.prototype.toString.call(e)==="[object Object]",ui=e=>e[e.length-1]==="/"?e.slice(0,-1):e,fi=e=>e[0]==="/"?e.slice(1):e,di=(e,t)=>{const n=Object.keys(e).sort((r,o)=>{const s=o.split("/").length-r.split("/").length;return s!==0?s:o.length-r.length});for(const r of n)if(t.startsWith(r))return r;return"/"};const hi={"v-fe360bd6":ve(()=>W(()=>import("./develop.html-5979c2ed.js"),[])),"v-d08d435a":ve(()=>W(()=>import("./introduce.html-56f089a5.js"),[])),"v-8daa1a0e":ve(()=>W(()=>import("./index.html-76732133.js"),[])),"v-7cbb39b9":ve(()=>W(()=>import("./api_case.html-69671d2d.js"),[])),"v-109501ec":ve(()=>W(()=>import("./api_object.html-931137ea.js"),[])),"v-d24a86f0":ve(()=>W(()=>import("./assert.html-82cebf94.js"),[])),"v-5b62ef19":ve(()=>W(()=>import("./more.html-65684271.js"),[])),"v-23f9483c":ve(()=>W(()=>import("./start.html-39c759f1.js"),[])),"v-488b8cec":ve(()=>W(()=>import("./webscocket.html-036a20ea.js"),[])),"v-53c50d87":ve(()=>W(()=>import("./appium_lab.html-8deae398.js"),[])),"v-483ce5fe":ve(()=>W(()=>import("./extensions.html-6bce6958.js"),[])),"v-299549e4":ve(()=>W(()=>import("./page_object.html-fd190005.js"),[])),"v-4370387b":ve(()=>W(()=>import("./start.html-75c073f4.js"),[])),"v-c350a662":ve(()=>W(()=>import("./db_operation.html-8c3713ed.js"),[])),"v-1d715ea7":ve(()=>W(()=>import("./test_library.html-2407429c.js"),[])),"v-d8f79a72":ve(()=>W(()=>import("./advanced.html-ef34530d.js"),[])),"v-8e520eda":ve(()=>W(()=>import("./create_project.html-9dc05755.js"),[])),"v-78c619cc":ve(()=>W(()=>import("./data_driver.html-f8fd9d76.js"),[])),"v-4e6f0425":ve(()=>W(()=>import("./dependent_func.html-bd1f5d4f.js"),[])),"v-4e8563af":ve(()=>W(()=>import("./installation.html-ce2cfb11.js"),[])),"v-0f898c79":ve(()=>W(()=>import("./quick_start.html-3d39b04b.js"),[])),"v-6f32df80":ve(()=>W(()=>import("./seldom_cli.html-3837c5f4.js"),[])),"v-3cdc5c3a":ve(()=>W(()=>import("./platform.html-7e431a87.js"),[])),"v-129a7066":ve(()=>W(()=>import("./CHANGES.html-5bee8a25.js"),[])),"v-471218ee":ve(()=>W(()=>import("./browser_driver.html-d273cebd.js"),[])),"v-198befa7":ve(()=>W(()=>import("./chaining.html-47993268.js"),[])),"v-1f2c830c":ve(()=>W(()=>import("./other.html-e43e8fc0.js"),[])),"v-0ccdb93b":ve(()=>W(()=>import("./page_object.html-d5443023.js"),[])),"v-40ed12b6":ve(()=>W(()=>import("./seldom_api.html-a8ef0a30.js"),[])),"v-3706649a":ve(()=>W(()=>import("./404.html-347f661a.js"),[]))};var Bu=Symbol(""),zu=me(Nu),pi=Bn({key:"",path:"",title:"",lang:"",frontmatter:{},headers:[]}),St=me(pi),Gt=()=>St,mi=Symbol(""),gt=()=>{const e=Pe(mi);if(!e)throw new Error("usePageFrontmatter() is called without provider.");return e},gi=Symbol(""),Vu=()=>{const e=Pe(gi);if(!e)throw new Error("usePageHead() is called without provider.");return e},Uu=Symbol(""),vi=Symbol(""),qu=()=>{const e=Pe(vi);if(!e)throw new Error("usePageLang() is called without provider.");return e},_i=Symbol(""),Wu=()=>{const e=Pe(_i);if(!e)throw new Error("usePageLayout() is called without provider.");return e},Do=Symbol(""),Kn=()=>{const e=Pe(Do);if(!e)throw new Error("useRouteLocale() is called without provider.");return e},nn=me(Du),bi=()=>nn,yi=Symbol(""),Mo=()=>{const e=Pe(yi);if(!e)throw new Error("useSiteLocaleData() is called without provider.");return e},Ku=Symbol(""),Gu="Layout",Yu="NotFound",ht=vn({resolveLayouts:e=>e.reduce((t,n)=>({...t,...n.layouts}),{}),resolvePageData:async e=>{const t=zu.value[e];return await(t==null?void 0:t())??pi},resolvePageFrontmatter:e=>e.frontmatter,resolvePageHead:(e,t,n)=>{const r=ge(t.description)?t.description:n.description,o=[...Q(t.head)?t.head:[],...n.head,["title",{},e],["meta",{name:"description",content:r}]];return Hu(o)},resolvePageHeadTitle:(e,t)=>[e.title,t.title].filter(n=>!!n).join(" | "),resolvePageLang:e=>e.lang||"en",resolvePageLayout:(e,t)=>{let n;if(e.path){const r=e.frontmatter.layout;ge(r)?n=r:n=Gu}else n=Yu;return t[n]},resolveRouteLocale:(e,t)=>di(e,t),resolveSiteLocaleData:(e,t)=>({...e,...e.locales[t]})}),Ho=ue({name:"ClientOnly",setup(e,t){const n=me(!1);return Je(()=>{n.value=!0}),()=>{var r,o;return n.value?(o=(r=t.slots).default)==null?void 0:o.call(r):null}}}),Ju=ue({name:"Content",props:{pageKey:{type:String,required:!1,default:""}},setup(e){const t=Gt(),n=F(()=>hi[e.pageKey||t.value.key]);return()=>n.value?ae(n.value):ae("div","404 Not Found")}}),Dt=(e={})=>e,Fo=e=>Wn(e)?e:`/${fi(e)}`;function Ei(e,t,n){var r,o,s;t===void 0&&(t=50),n===void 0&&(n={});var l=(r=n.isImmediate)!=null&&r,i=(o=n.callback)!=null&&o,a=n.maxWait,c=Date.now(),u=[];function f(){if(a!==void 0){var g=Date.now()-c;if(g+t>=a)return a-g}return t}var h=function(){var g=[].slice.call(arguments),y=this;return new Promise(function(w,C){var v=l&&s===void 0;if(s!==void 0&&clearTimeout(s),s=setTimeout(function(){if(s=void 0,c=Date.now(),!l){var P=e.apply(y,g);i&&i(P),u.forEach(function(S){return(0,S.resolve)(P)}),u=[]}},f()),v){var b=e.apply(y,g);return i&&i(b),w(b)}u.push({resolve:w,reject:C})})};return h.cancel=function(g){s!==void 0&&clearTimeout(s),u.forEach(function(y){return(0,y.reject)(g)}),u=[]},h}/*! - * vue-router v4.2.2 - * (c) 2023 Eduardo San Martin Morote - * @license MIT - */const tn=typeof window<"u";function Qu(e){return e.__esModule||e[Symbol.toStringTag]==="Module"}const _e=Object.assign;function Kr(e,t){const n={};for(const r in t){const o=t[r];n[r]=lt(o)?o.map(e):e(o)}return n}const Pn=()=>{},lt=Array.isArray,Zu=/\/$/,Xu=e=>e.replace(Zu,"");function Gr(e,t,n="/"){let r,o={},s="",l="";const i=t.indexOf("#");let a=t.indexOf("?");return i=0&&(a=-1),a>-1&&(r=t.slice(0,a),s=t.slice(a+1,i>-1?i:t.length),o=e(s)),i>-1&&(r=r||t.slice(0,i),l=t.slice(i,t.length)),r=rf(r??t,n),{fullPath:r+(s&&"?")+s+l,path:r,query:o,hash:l}}function ef(e,t){const n=t.query?e(t.query):"";return t.path+(n&&"?")+n+(t.hash||"")}function Is(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||"/"}function tf(e,t,n){const r=t.matched.length-1,o=n.matched.length-1;return r>-1&&r===o&&hn(t.matched[r],n.matched[o])&&wi(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function hn(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function wi(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(const n in e)if(!nf(e[n],t[n]))return!1;return!0}function nf(e,t){return lt(e)?$s(e,t):lt(t)?$s(t,e):e===t}function $s(e,t){return lt(t)?e.length===t.length&&e.every((n,r)=>n===t[r]):e.length===1&&e[0]===t}function rf(e,t){if(e.startsWith("/"))return e;if(!e)return t;const n=t.split("/"),r=e.split("/"),o=r[r.length-1];(o===".."||o===".")&&r.push("");let s=n.length-1,l,i;for(l=0;l1&&s--;else break;return n.slice(0,s).join("/")+"/"+r.slice(l-(l===r.length?1:0)).join("/")}var Dn;(function(e){e.pop="pop",e.push="push"})(Dn||(Dn={}));var An;(function(e){e.back="back",e.forward="forward",e.unknown=""})(An||(An={}));function of(e){if(!e)if(tn){const t=document.querySelector("base");e=t&&t.getAttribute("href")||"/",e=e.replace(/^\w+:\/\/[^\/]+/,"")}else e="/";return e[0]!=="/"&&e[0]!=="#"&&(e="/"+e),Xu(e)}const sf=/^[^#]+#/;function lf(e,t){return e.replace(sf,"#")+t}function af(e,t){const n=document.documentElement.getBoundingClientRect(),r=e.getBoundingClientRect();return{behavior:t.behavior,left:r.left-n.left-(t.left||0),top:r.top-n.top-(t.top||0)}}const Nr=()=>({left:window.pageXOffset,top:window.pageYOffset});function cf(e){let t;if("el"in e){const n=e.el,r=typeof n=="string"&&n.startsWith("#"),o=typeof n=="string"?r?document.getElementById(n.slice(1)):document.querySelector(n):n;if(!o)return;t=af(o,e)}else t=e;"scrollBehavior"in document.documentElement.style?window.scrollTo(t):window.scrollTo(t.left!=null?t.left:window.pageXOffset,t.top!=null?t.top:window.pageYOffset)}function Ns(e,t){return(history.state?history.state.position-t:-1)+e}const fo=new Map;function uf(e,t){fo.set(e,t)}function ff(e){const t=fo.get(e);return fo.delete(e),t}let df=()=>location.protocol+"//"+location.host;function ki(e,t){const{pathname:n,search:r,hash:o}=t,s=e.indexOf("#");if(s>-1){let i=o.includes(e.slice(s))?e.slice(s).length:1,a=o.slice(i);return a[0]!=="/"&&(a="/"+a),Is(a,"")}return Is(n,e)+r+o}function hf(e,t,n,r){let o=[],s=[],l=null;const i=({state:h})=>{const g=ki(e,location),y=n.value,w=t.value;let C=0;if(h){if(n.value=g,t.value=h,l&&l===y){l=null;return}C=w?h.position-w.position:0}else r(g);o.forEach(v=>{v(n.value,y,{delta:C,type:Dn.pop,direction:C?C>0?An.forward:An.back:An.unknown})})};function a(){l=n.value}function c(h){o.push(h);const g=()=>{const y=o.indexOf(h);y>-1&&o.splice(y,1)};return s.push(g),g}function u(){const{history:h}=window;h.state&&h.replaceState(_e({},h.state,{scroll:Nr()}),"")}function f(){for(const h of s)h();s=[],window.removeEventListener("popstate",i),window.removeEventListener("beforeunload",u)}return window.addEventListener("popstate",i),window.addEventListener("beforeunload",u,{passive:!0}),{pauseListeners:a,listen:c,destroy:f}}function Ds(e,t,n,r=!1,o=!1){return{back:e,current:t,forward:n,replaced:r,position:window.history.length,scroll:o?Nr():null}}function pf(e){const{history:t,location:n}=window,r={value:ki(e,n)},o={value:t.state};o.value||s(r.value,{back:null,current:r.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function s(a,c,u){const f=e.indexOf("#"),h=f>-1?(n.host&&document.querySelector("base")?e:e.slice(f))+a:df()+e+a;try{t[u?"replaceState":"pushState"](c,"",h),o.value=c}catch(g){console.error(g),n[u?"replace":"assign"](h)}}function l(a,c){const u=_e({},t.state,Ds(o.value.back,a,o.value.forward,!0),c,{position:o.value.position});s(a,u,!0),r.value=a}function i(a,c){const u=_e({},o.value,t.state,{forward:a,scroll:Nr()});s(u.current,u,!0);const f=_e({},Ds(r.value,a,null),{position:u.position+1},c);s(a,f,!1),r.value=a}return{location:r,state:o,push:i,replace:l}}function mf(e){e=of(e);const t=pf(e),n=hf(e,t.state,t.location,t.replace);function r(s,l=!0){l||n.pauseListeners(),history.go(s)}const o=_e({location:"",base:e,go:r,createHref:lf.bind(null,e)},t,n);return Object.defineProperty(o,"location",{enumerable:!0,get:()=>t.location.value}),Object.defineProperty(o,"state",{enumerable:!0,get:()=>t.state.value}),o}function gf(e){return typeof e=="string"||e&&typeof e=="object"}function xi(e){return typeof e=="string"||typeof e=="symbol"}const pt={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0},Li=Symbol("");var Ms;(function(e){e[e.aborted=4]="aborted",e[e.cancelled=8]="cancelled",e[e.duplicated=16]="duplicated"})(Ms||(Ms={}));function pn(e,t){return _e(new Error,{type:e,[Li]:!0},t)}function dt(e,t){return e instanceof Error&&Li in e&&(t==null||!!(e.type&t))}const Hs="[^/]+?",vf={sensitive:!1,strict:!1,start:!0,end:!0},_f=/[.+*?^${}()[\]/\\]/g;function bf(e,t){const n=_e({},vf,t),r=[];let o=n.start?"^":"";const s=[];for(const c of e){const u=c.length?[]:[90];n.strict&&!c.length&&(o+="/");for(let f=0;ft.length?t.length===1&&t[0]===40+40?1:-1:0}function Ef(e,t){let n=0;const r=e.score,o=t.score;for(;n0&&t[t.length-1]<0}const wf={type:0,value:""},kf=/[a-zA-Z0-9_]/;function xf(e){if(!e)return[[]];if(e==="/")return[[wf]];if(!e.startsWith("/"))throw new Error(`Invalid path "${e}"`);function t(g){throw new Error(`ERR (${n})/"${c}": ${g}`)}let n=0,r=n;const o=[];let s;function l(){s&&o.push(s),s=[]}let i=0,a,c="",u="";function f(){c&&(n===0?s.push({type:0,value:c}):n===1||n===2||n===3?(s.length>1&&(a==="*"||a==="+")&&t(`A repeatable param (${c}) must be alone in its segment. eg: '/:ids+.`),s.push({type:1,value:c,regexp:u,repeatable:a==="*"||a==="+",optional:a==="*"||a==="?"})):t("Invalid state to consume buffer"),c="")}function h(){c+=a}for(;i{l(b)}:Pn}function l(u){if(xi(u)){const f=r.get(u);f&&(r.delete(u),n.splice(n.indexOf(f),1),f.children.forEach(l),f.alias.forEach(l))}else{const f=n.indexOf(u);f>-1&&(n.splice(f,1),u.record.name&&r.delete(u.record.name),u.children.forEach(l),u.alias.forEach(l))}}function i(){return n}function a(u){let f=0;for(;f=0&&(u.record.path!==n[f].record.path||!Ci(u,n[f]));)f++;n.splice(f,0,u),u.record.name&&!Bs(u)&&r.set(u.record.name,u)}function c(u,f){let h,g={},y,w;if("name"in u&&u.name){if(h=r.get(u.name),!h)throw pn(1,{location:u});w=h.record.name,g=_e(js(f.params,h.keys.filter(b=>!b.optional).map(b=>b.name)),u.params&&js(u.params,h.keys.map(b=>b.name))),y=h.stringify(g)}else if("path"in u)y=u.path,h=n.find(b=>b.re.test(y)),h&&(g=h.parse(y),w=h.record.name);else{if(h=f.name?r.get(f.name):n.find(b=>b.re.test(f.path)),!h)throw pn(1,{location:u,currentLocation:f});w=h.record.name,g=_e({},f.params,u.params),y=h.stringify(g)}const C=[];let v=h;for(;v;)C.unshift(v.record),v=v.parent;return{name:w,path:y,params:g,matched:C,meta:Pf(C)}}return e.forEach(u=>s(u)),{addRoute:s,resolve:c,removeRoute:l,getRoutes:i,getRecordMatcher:o}}function js(e,t){const n={};for(const r of t)r in e&&(n[r]=e[r]);return n}function Tf(e){return{path:e.path,redirect:e.redirect,name:e.name,meta:e.meta||{},aliasOf:void 0,beforeEnter:e.beforeEnter,props:Sf(e),children:e.children||[],instances:{},leaveGuards:new Set,updateGuards:new Set,enterCallbacks:{},components:"components"in e?e.components||null:e.component&&{default:e.component}}}function Sf(e){const t={},n=e.props||!1;if("component"in e)t.default=n;else for(const r in e.components)t[r]=typeof n=="boolean"?n:n[r];return t}function Bs(e){for(;e;){if(e.record.aliasOf)return!0;e=e.parent}return!1}function Pf(e){return e.reduce((t,n)=>_e(t,n.meta),{})}function zs(e,t){const n={};for(const r in e)n[r]=r in t?t[r]:e[r];return n}function Ci(e,t){return t.children.some(n=>n===e||Ci(e,n))}const Ti=/#/g,Af=/&/g,Of=/\//g,Rf=/=/g,If=/\?/g,Si=/\+/g,$f=/%5B/g,Nf=/%5D/g,Pi=/%5E/g,Df=/%60/g,Ai=/%7B/g,Mf=/%7C/g,Oi=/%7D/g,Hf=/%20/g;function jo(e){return encodeURI(""+e).replace(Mf,"|").replace($f,"[").replace(Nf,"]")}function Ff(e){return jo(e).replace(Ai,"{").replace(Oi,"}").replace(Pi,"^")}function ho(e){return jo(e).replace(Si,"%2B").replace(Hf,"+").replace(Ti,"%23").replace(Af,"%26").replace(Df,"`").replace(Ai,"{").replace(Oi,"}").replace(Pi,"^")}function jf(e){return ho(e).replace(Rf,"%3D")}function Bf(e){return jo(e).replace(Ti,"%23").replace(If,"%3F")}function zf(e){return e==null?"":Bf(e).replace(Of,"%2F")}function yr(e){try{return decodeURIComponent(""+e)}catch{}return""+e}function Vf(e){const t={};if(e===""||e==="?")return t;const r=(e[0]==="?"?e.slice(1):e).split("&");for(let o=0;os&&ho(s)):[r&&ho(r)]).forEach(s=>{s!==void 0&&(t+=(t.length?"&":"")+n,s!=null&&(t+="="+s))})}return t}function Uf(e){const t={};for(const n in e){const r=e[n];r!==void 0&&(t[n]=lt(r)?r.map(o=>o==null?null:""+o):r==null?r:""+r)}return t}const qf=Symbol(""),Us=Symbol(""),Dr=Symbol(""),Bo=Symbol(""),po=Symbol("");function En(){let e=[];function t(r){return e.push(r),()=>{const o=e.indexOf(r);o>-1&&e.splice(o,1)}}function n(){e=[]}return{add:t,list:()=>e,reset:n}}function Pt(e,t,n,r,o){const s=r&&(r.enterCallbacks[o]=r.enterCallbacks[o]||[]);return()=>new Promise((l,i)=>{const a=f=>{f===!1?i(pn(4,{from:n,to:t})):f instanceof Error?i(f):gf(f)?i(pn(2,{from:t,to:f})):(s&&r.enterCallbacks[o]===s&&typeof f=="function"&&s.push(f),l())},c=e.call(r&&r.instances[o],t,n,a);let u=Promise.resolve(c);e.length<3&&(u=u.then(a)),u.catch(f=>i(f))})}function Yr(e,t,n,r){const o=[];for(const s of e)for(const l in s.components){let i=s.components[l];if(!(t!=="beforeRouteEnter"&&!s.instances[l]))if(Wf(i)){const c=(i.__vccOpts||i)[t];c&&o.push(Pt(c,n,r,s,l))}else{let a=i();o.push(()=>a.then(c=>{if(!c)return Promise.reject(new Error(`Couldn't resolve component "${l}" at "${s.path}"`));const u=Qu(c)?c.default:c;s.components[l]=u;const h=(u.__vccOpts||u)[t];return h&&Pt(h,n,r,s,l)()}))}}return o}function Wf(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function qs(e){const t=Pe(Dr),n=Pe(Bo),r=F(()=>t.resolve(ee(e.to))),o=F(()=>{const{matched:a}=r.value,{length:c}=a,u=a[c-1],f=n.matched;if(!u||!f.length)return-1;const h=f.findIndex(hn.bind(null,u));if(h>-1)return h;const g=Ws(a[c-2]);return c>1&&Ws(u)===g&&f[f.length-1].path!==g?f.findIndex(hn.bind(null,a[c-2])):h}),s=F(()=>o.value>-1&&Jf(n.params,r.value.params)),l=F(()=>o.value>-1&&o.value===n.matched.length-1&&wi(n.params,r.value.params));function i(a={}){return Yf(a)?t[ee(e.replace)?"replace":"push"](ee(e.to)).catch(Pn):Promise.resolve()}return{route:r,href:F(()=>r.value.href),isActive:s,isExactActive:l,navigate:i}}const Kf=ue({name:"RouterLink",compatConfig:{MODE:3},props:{to:{type:[String,Object],required:!0},replace:Boolean,activeClass:String,exactActiveClass:String,custom:Boolean,ariaCurrentValue:{type:String,default:"page"}},useLink:qs,setup(e,{slots:t}){const n=vn(qs(e)),{options:r}=Pe(Dr),o=F(()=>({[Ks(e.activeClass,r.linkActiveClass,"router-link-active")]:n.isActive,[Ks(e.exactActiveClass,r.linkExactActiveClass,"router-link-exact-active")]:n.isExactActive}));return()=>{const s=t.default&&t.default(n);return e.custom?s:ae("a",{"aria-current":n.isExactActive?e.ariaCurrentValue:null,href:n.href,onClick:n.navigate,class:o.value},s)}}}),Gf=Kf;function Yf(e){if(!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)&&!e.defaultPrevented&&!(e.button!==void 0&&e.button!==0)){if(e.currentTarget&&e.currentTarget.getAttribute){const t=e.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(t))return}return e.preventDefault&&e.preventDefault(),!0}}function Jf(e,t){for(const n in t){const r=t[n],o=e[n];if(typeof r=="string"){if(r!==o)return!1}else if(!lt(o)||o.length!==r.length||r.some((s,l)=>s!==o[l]))return!1}return!0}function Ws(e){return e?e.aliasOf?e.aliasOf.path:e.path:""}const Ks=(e,t,n)=>e??t??n,Qf=ue({name:"RouterView",inheritAttrs:!1,props:{name:{type:String,default:"default"},route:Object},compatConfig:{MODE:3},setup(e,{attrs:t,slots:n}){const r=Pe(po),o=F(()=>e.route||r.value),s=Pe(Us,0),l=F(()=>{let c=ee(s);const{matched:u}=o.value;let f;for(;(f=u[c])&&!f.components;)c++;return c}),i=F(()=>o.value.matched[l.value]);Wt(Us,F(()=>l.value+1)),Wt(qf,i),Wt(po,o);const a=me();return st(()=>[a.value,i.value,e.name],([c,u,f],[h,g,y])=>{u&&(u.instances[f]=c,g&&g!==u&&c&&c===h&&(u.leaveGuards.size||(u.leaveGuards=g.leaveGuards),u.updateGuards.size||(u.updateGuards=g.updateGuards))),c&&u&&(!g||!hn(u,g)||!h)&&(u.enterCallbacks[f]||[]).forEach(w=>w(c))},{flush:"post"}),()=>{const c=o.value,u=e.name,f=i.value,h=f&&f.components[u];if(!h)return Gs(n.default,{Component:h,route:c});const g=f.props[u],y=g?g===!0?c.params:typeof g=="function"?g(c):g:null,C=ae(h,_e({},y,t,{onVnodeUnmounted:v=>{v.component.isUnmounted&&(f.instances[u]=null)},ref:a}));return Gs(n.default,{Component:C,route:c})||C}}});function Gs(e,t){if(!e)return null;const n=e(t);return n.length===1?n[0]:n}const Ri=Qf;function Zf(e){const t=Cf(e.routes,e),n=e.parseQuery||Vf,r=e.stringifyQuery||Vs,o=e.history,s=En(),l=En(),i=En(),a=Il(pt);let c=pt;tn&&e.scrollBehavior&&"scrollRestoration"in history&&(history.scrollRestoration="manual");const u=Kr.bind(null,k=>""+k),f=Kr.bind(null,zf),h=Kr.bind(null,yr);function g(k,B){let N,K;return xi(k)?(N=t.getRecordMatcher(k),K=B):K=k,t.addRoute(K,N)}function y(k){const B=t.getRecordMatcher(k);B&&t.removeRoute(B)}function w(){return t.getRoutes().map(k=>k.record)}function C(k){return!!t.getRecordMatcher(k)}function v(k,B){if(B=_e({},B||a.value),typeof k=="string"){const _=Gr(n,k,B.path),E=t.resolve({path:_.path},B),x=o.createHref(_.fullPath);return _e(_,E,{params:h(E.params),hash:yr(_.hash),redirectedFrom:void 0,href:x})}let N;if("path"in k)N=_e({},k,{path:Gr(n,k.path,B.path).path});else{const _=_e({},k.params);for(const E in _)_[E]==null&&delete _[E];N=_e({},k,{params:f(_)}),B.params=f(B.params)}const K=t.resolve(N,B),ce=k.hash||"";K.params=u(h(K.params));const d=ef(r,_e({},k,{hash:Ff(ce),path:K.path})),p=o.createHref(d);return _e({fullPath:d,hash:ce,query:r===Vs?Uf(k.query):k.query||{}},K,{redirectedFrom:void 0,href:p})}function b(k){return typeof k=="string"?Gr(n,k,a.value.path):_e({},k)}function P(k,B){if(c!==k)return pn(8,{from:B,to:k})}function S(k){return $(k)}function U(k){return S(_e(b(k),{replace:!0}))}function Z(k){const B=k.matched[k.matched.length-1];if(B&&B.redirect){const{redirect:N}=B;let K=typeof N=="function"?N(k):N;return typeof K=="string"&&(K=K.includes("?")||K.includes("#")?K=b(K):{path:K},K.params={}),_e({query:k.query,hash:k.hash,params:"path"in K?{}:k.params},K)}}function $(k,B){const N=c=v(k),K=a.value,ce=k.state,d=k.force,p=k.replace===!0,_=Z(N);if(_)return $(_e(b(_),{state:typeof _=="object"?_e({},ce,_.state):ce,force:d,replace:p}),B||N);const E=N;E.redirectedFrom=B;let x;return!d&&tf(r,K,N)&&(x=pn(16,{to:E,from:K}),Ve(K,K,!0,!1)),(x?Promise.resolve(x):M(E,K)).catch(T=>dt(T)?dt(T,2)?T:Ne(T):se(T,E,K)).then(T=>{if(T){if(dt(T,2))return $(_e({replace:p},b(T.to),{state:typeof T.to=="object"?_e({},ce,T.to.state):ce,force:d}),B||E)}else T=L(E,K,!0,p,ce);return G(E,K,T),T})}function m(k,B){const N=P(k,B);return N?Promise.reject(N):Promise.resolve()}function z(k){const B=wt.values().next().value;return B&&typeof B.runWithContext=="function"?B.runWithContext(k):k()}function M(k,B){let N;const[K,ce,d]=Xf(k,B);N=Yr(K.reverse(),"beforeRouteLeave",k,B);for(const _ of K)_.leaveGuards.forEach(E=>{N.push(Pt(E,k,B))});const p=m.bind(null,k,B);return N.push(p),$e(N).then(()=>{N=[];for(const _ of s.list())N.push(Pt(_,k,B));return N.push(p),$e(N)}).then(()=>{N=Yr(ce,"beforeRouteUpdate",k,B);for(const _ of ce)_.updateGuards.forEach(E=>{N.push(Pt(E,k,B))});return N.push(p),$e(N)}).then(()=>{N=[];for(const _ of k.matched)if(_.beforeEnter&&!B.matched.includes(_))if(lt(_.beforeEnter))for(const E of _.beforeEnter)N.push(Pt(E,k,B));else N.push(Pt(_.beforeEnter,k,B));return N.push(p),$e(N)}).then(()=>(k.matched.forEach(_=>_.enterCallbacks={}),N=Yr(d,"beforeRouteEnter",k,B),N.push(p),$e(N))).then(()=>{N=[];for(const _ of l.list())N.push(Pt(_,k,B));return N.push(p),$e(N)}).catch(_=>dt(_,8)?_:Promise.reject(_))}function G(k,B,N){for(const K of i.list())z(()=>K(k,B,N))}function L(k,B,N,K,ce){const d=P(k,B);if(d)return d;const p=B===pt,_=tn?history.state:{};N&&(K||p?o.replace(k.fullPath,_e({scroll:p&&_&&_.scroll},ce)):o.push(k.fullPath,ce)),a.value=k,Ve(k,B,N,p),Ne()}let R;function I(){R||(R=o.listen((k,B,N)=>{if(!it.listening)return;const K=v(k),ce=Z(K);if(ce){$(_e(ce,{replace:!0}),K).catch(Pn);return}c=K;const d=a.value;tn&&uf(Ns(d.fullPath,N.delta),Nr()),M(K,d).catch(p=>dt(p,12)?p:dt(p,2)?($(p.to,K).then(_=>{dt(_,20)&&!N.delta&&N.type===Dn.pop&&o.go(-1,!1)}).catch(Pn),Promise.reject()):(N.delta&&o.go(-N.delta,!1),se(p,K,d))).then(p=>{p=p||L(K,d,!1),p&&(N.delta&&!dt(p,8)?o.go(-N.delta,!1):N.type===Dn.pop&&dt(p,20)&&o.go(-1,!1)),G(K,d,p)}).catch(Pn)}))}let le=En(),V=En(),re;function se(k,B,N){Ne(k);const K=V.list();return K.length?K.forEach(ce=>ce(k,B,N)):console.error(k),Promise.reject(k)}function He(){return re&&a.value!==pt?Promise.resolve():new Promise((k,B)=>{le.add([k,B])})}function Ne(k){return re||(re=!k,I(),le.list().forEach(([B,N])=>k?N(k):B()),le.reset()),k}function Ve(k,B,N,K){const{scrollBehavior:ce}=e;if(!tn||!ce)return Promise.resolve();const d=!N&&ff(Ns(k.fullPath,0))||(K||!N)&&history.state&&history.state.scroll||null;return Sr().then(()=>ce(k,B,d)).then(p=>p&&cf(p)).catch(p=>se(p,k,B))}const Fe=k=>o.go(k);let Et;const wt=new Set,it={currentRoute:a,listening:!0,addRoute:g,removeRoute:y,hasRoute:C,getRoutes:w,resolve:v,options:e,push:S,replace:U,go:Fe,back:()=>Fe(-1),forward:()=>Fe(1),beforeEach:s.add,beforeResolve:l.add,afterEach:i.add,onError:V.add,isReady:He,install(k){const B=this;k.component("RouterLink",Gf),k.component("RouterView",Ri),k.config.globalProperties.$router=B,Object.defineProperty(k.config.globalProperties,"$route",{enumerable:!0,get:()=>ee(a)}),tn&&!Et&&a.value===pt&&(Et=!0,S(o.location).catch(ce=>{}));const N={};for(const ce in pt)N[ce]=F(()=>a.value[ce]);k.provide(Dr,B),k.provide(Bo,vn(N)),k.provide(po,a);const K=k.unmount;wt.add(k),k.unmount=function(){wt.delete(k),wt.size<1&&(c=pt,R&&R(),R=null,a.value=pt,Et=!1,re=!1),K()}}};function $e(k){return k.reduce((B,N)=>B.then(()=>z(N)),Promise.resolve())}return it}function Xf(e,t){const n=[],r=[],o=[],s=Math.max(t.matched.length,e.matched.length);for(let l=0;lhn(c,i))?r.push(i):n.push(i));const a=e.matched[l];a&&(t.matched.find(c=>hn(c,a))||o.push(a))}return[n,r,o]}function Jt(){return Pe(Dr)}function Qt(){return Pe(Bo)}const ed=({headerLinkSelector:e,headerAnchorSelector:t,delay:n,offset:r=5})=>{const o=Jt(),l=Ei(()=>{var w,C;const i=Math.max(window.scrollY,document.documentElement.scrollTop,document.body.scrollTop);if(Math.abs(i-0)h.some(b=>b.hash===v.hash));for(let v=0;v=(((w=b.parentElement)==null?void 0:w.offsetTop)??0)-r,U=!P||i<(((C=P.parentElement)==null?void 0:C.offsetTop)??0)-r;if(!(S&&U))continue;const $=decodeURIComponent(o.currentRoute.value.hash),m=decodeURIComponent(b.hash);if($===m)return;if(f){for(let z=v+1;z{window.addEventListener("scroll",l)}),Un(()=>{window.removeEventListener("scroll",l)})},Ys=async(e,t)=>{const{scrollBehavior:n}=e.options;e.options.scrollBehavior=void 0,await e.replace({query:e.currentRoute.value.query,hash:t,force:!0}).finally(()=>e.options.scrollBehavior=n)},td="a.sidebar-item",nd=".header-anchor",rd=300,od=5,sd=Dt({setup(){ed({headerLinkSelector:td,headerAnchorSelector:nd,delay:rd,offset:od})}});const ld=ae("svg",{class:"external-link-icon",xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",focusable:"false",x:"0px",y:"0px",viewBox:"0 0 100 100",width:"15",height:"15"},[ae("path",{fill:"currentColor",d:"M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"}),ae("polygon",{fill:"currentColor",points:"45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"})]),id=ue({name:"ExternalLinkIcon",props:{locales:{type:Object,required:!1,default:()=>({})}},setup(e){const t=Kn(),n=F(()=>e.locales[t.value]??{openInNewWindow:"open in new window"});return()=>ae("span",[ld,ae("span",{class:"external-link-icon-sr-only"},n.value.openInNewWindow)])}}),ad={"/":{openInNewWindow:"open in new window"}},cd=Dt({enhance({app:e}){e.component("ExternalLinkIcon",ae(id,{locales:ad}))}});/*! medium-zoom 1.0.8 | MIT License | https://github.com/francoischalifour/medium-zoom */var jt=Object.assign||function(e){for(var t=1;t1&&arguments[1]!==void 0?arguments[1]:{},r=window.Promise||function(L){function R(){}L(R,R)},o=function(L){var R=L.target;if(R===z){y();return}P.indexOf(R)!==-1&&w({target:R})},s=function(){if(!(U||!m.original)){var L=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0;Math.abs(Z-L)>$.scrollOffset&&setTimeout(y,150)}},l=function(L){var R=L.key||L.keyCode;(R==="Escape"||R==="Esc"||R===27)&&y()},i=function(){var L=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},R=L;if(L.background&&(z.style.background=L.background),L.container&&L.container instanceof Object&&(R.container=jt({},$.container,L.container)),L.template){var I=ar(L.template)?L.template:document.querySelector(L.template);R.template=I}return $=jt({},$,R),P.forEach(function(le){le.dispatchEvent(en("medium-zoom:update",{detail:{zoom:M}}))}),M},a=function(){var L=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};return e(jt({},$,L))},c=function(){for(var L=arguments.length,R=Array(L),I=0;I0?R.reduce(function(V,re){return[].concat(V,Qs(re))},[]):P;return le.forEach(function(V){V.classList.remove("medium-zoom-image"),V.dispatchEvent(en("medium-zoom:detach",{detail:{zoom:M}}))}),P=P.filter(function(V){return le.indexOf(V)===-1}),M},f=function(L,R){var I=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{};return P.forEach(function(le){le.addEventListener("medium-zoom:"+L,R,I)}),S.push({type:"medium-zoom:"+L,listener:R,options:I}),M},h=function(L,R){var I=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{};return P.forEach(function(le){le.removeEventListener("medium-zoom:"+L,R,I)}),S=S.filter(function(le){return!(le.type==="medium-zoom:"+L&&le.listener.toString()===R.toString())}),M},g=function(){var L=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},R=L.target,I=function(){var V={width:document.documentElement.clientWidth,height:document.documentElement.clientHeight,left:0,top:0,right:0,bottom:0},re=void 0,se=void 0;if($.container)if($.container instanceof Object)V=jt({},V,$.container),re=V.width-V.left-V.right-$.margin*2,se=V.height-V.top-V.bottom-$.margin*2;else{var He=ar($.container)?$.container:document.querySelector($.container),Ne=He.getBoundingClientRect(),Ve=Ne.width,Fe=Ne.height,Et=Ne.left,wt=Ne.top;V=jt({},V,{width:Ve,height:Fe,left:Et,top:wt})}re=re||V.width-$.margin*2,se=se||V.height-$.margin*2;var it=m.zoomedHd||m.original,$e=Js(it)?re:it.naturalWidth||re,k=Js(it)?se:it.naturalHeight||se,B=it.getBoundingClientRect(),N=B.top,K=B.left,ce=B.width,d=B.height,p=Math.min(Math.max(ce,$e),re)/ce,_=Math.min(Math.max(d,k),se)/d,E=Math.min(p,_),x=(-K+(re-ce)/2+$.margin+V.left)/E,T=(-N+(se-d)/2+$.margin+V.top)/E,H="scale("+E+") translate3d("+x+"px, "+T+"px, 0)";m.zoomed.style.transform=H,m.zoomedHd&&(m.zoomedHd.style.transform=H)};return new r(function(le){if(R&&P.indexOf(R)===-1){le(M);return}var V=function Ve(){U=!1,m.zoomed.removeEventListener("transitionend",Ve),m.original.dispatchEvent(en("medium-zoom:opened",{detail:{zoom:M}})),le(M)};if(m.zoomed){le(M);return}if(R)m.original=R;else if(P.length>0){var re=P;m.original=re[0]}else{le(M);return}if(m.original.dispatchEvent(en("medium-zoom:open",{detail:{zoom:M}})),Z=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0,U=!0,m.zoomed=dd(m.original),document.body.appendChild(z),$.template){var se=ar($.template)?$.template:document.querySelector($.template);m.template=document.createElement("div"),m.template.appendChild(se.content.cloneNode(!0)),document.body.appendChild(m.template)}if(m.original.parentElement&&m.original.parentElement.tagName==="PICTURE"&&m.original.currentSrc&&(m.zoomed.src=m.original.currentSrc),document.body.appendChild(m.zoomed),window.requestAnimationFrame(function(){document.body.classList.add("medium-zoom--opened")}),m.original.classList.add("medium-zoom-image--hidden"),m.zoomed.classList.add("medium-zoom-image--opened"),m.zoomed.addEventListener("click",y),m.zoomed.addEventListener("transitionend",V),m.original.getAttribute("data-zoom-src")){m.zoomedHd=m.zoomed.cloneNode(),m.zoomedHd.removeAttribute("srcset"),m.zoomedHd.removeAttribute("sizes"),m.zoomedHd.removeAttribute("loading"),m.zoomedHd.src=m.zoomed.getAttribute("data-zoom-src"),m.zoomedHd.onerror=function(){clearInterval(He),console.warn("Unable to reach the zoom image target "+m.zoomedHd.src),m.zoomedHd=null,I()};var He=setInterval(function(){m.zoomedHd.complete&&(clearInterval(He),m.zoomedHd.classList.add("medium-zoom-image--opened"),m.zoomedHd.addEventListener("click",y),document.body.appendChild(m.zoomedHd),I())},10)}else if(m.original.hasAttribute("srcset")){m.zoomedHd=m.zoomed.cloneNode(),m.zoomedHd.removeAttribute("sizes"),m.zoomedHd.removeAttribute("loading");var Ne=m.zoomedHd.addEventListener("load",function(){m.zoomedHd.removeEventListener("load",Ne),m.zoomedHd.classList.add("medium-zoom-image--opened"),m.zoomedHd.addEventListener("click",y),document.body.appendChild(m.zoomedHd),I()})}else I()})},y=function(){return new r(function(L){if(U||!m.original){L(M);return}var R=function I(){m.original.classList.remove("medium-zoom-image--hidden"),document.body.removeChild(m.zoomed),m.zoomedHd&&document.body.removeChild(m.zoomedHd),document.body.removeChild(z),m.zoomed.classList.remove("medium-zoom-image--opened"),m.template&&document.body.removeChild(m.template),U=!1,m.zoomed.removeEventListener("transitionend",I),m.original.dispatchEvent(en("medium-zoom:closed",{detail:{zoom:M}})),m.original=null,m.zoomed=null,m.zoomedHd=null,m.template=null,L(M)};U=!0,document.body.classList.remove("medium-zoom--opened"),m.zoomed.style.transform="",m.zoomedHd&&(m.zoomedHd.style.transform=""),m.template&&(m.template.style.transition="opacity 150ms",m.template.style.opacity=0),m.original.dispatchEvent(en("medium-zoom:close",{detail:{zoom:M}})),m.zoomed.addEventListener("transitionend",R)})},w=function(){var L=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},R=L.target;return m.original?y():g({target:R})},C=function(){return $},v=function(){return P},b=function(){return m.original},P=[],S=[],U=!1,Z=0,$=n,m={original:null,zoomed:null,zoomedHd:null,template:null};Object.prototype.toString.call(t)==="[object Object]"?$=t:(t||typeof t=="string")&&c(t),$=jt({margin:0,background:"#fff",scrollOffset:40,container:null,template:null},$);var z=fd($.background);document.addEventListener("click",o),document.addEventListener("keyup",l),document.addEventListener("scroll",s),window.addEventListener("resize",y);var M={open:g,close:y,toggle:w,update:i,clone:a,attach:c,detach:u,on:f,off:h,getOptions:C,getImages:v,getZoomedImage:b};return M};function pd(e,t){t===void 0&&(t={});var n=t.insertAt;if(!(!e||typeof document>"u")){var r=document.head||document.getElementsByTagName("head")[0],o=document.createElement("style");o.type="text/css",n==="top"&&r.firstChild?r.insertBefore(o,r.firstChild):r.appendChild(o),o.styleSheet?o.styleSheet.cssText=e:o.appendChild(document.createTextNode(e))}}var md=".medium-zoom-overlay{position:fixed;top:0;right:0;bottom:0;left:0;opacity:0;transition:opacity .3s;will-change:opacity}.medium-zoom--opened .medium-zoom-overlay{cursor:pointer;cursor:zoom-out;opacity:1}.medium-zoom-image{cursor:pointer;cursor:zoom-in;transition:transform .3s cubic-bezier(.2,0,.2,1)!important}.medium-zoom-image--hidden{visibility:hidden}.medium-zoom-image--opened{position:relative;cursor:pointer;cursor:zoom-out;will-change:transform}";pd(md);const gd=hd,vd=Symbol("mediumZoom");const _d=".theme-default-content > img, .theme-default-content :not(a) > img",bd={},yd=300,Ed=Dt({enhance({app:e,router:t}){const n=gd(bd);n.refresh=(r=_d)=>{n.detach(),n.attach(r)},e.provide(vd,n),t.afterEach(()=>{setTimeout(()=>n.refresh(),yd)})}});/** - * NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress - * @license MIT - */const fe={settings:{minimum:.08,easing:"ease",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,barSelector:'[role="bar"]',parent:"body",template:'
    '},status:null,set:e=>{const t=fe.isStarted();e=Jr(e,fe.settings.minimum,1),fe.status=e===1?null:e;const n=fe.render(!t),r=n.querySelector(fe.settings.barSelector),o=fe.settings.speed,s=fe.settings.easing;return n.offsetWidth,wd(l=>{rr(r,{transform:"translate3d("+Zs(e)+"%,0,0)",transition:"all "+o+"ms "+s}),e===1?(rr(n,{transition:"none",opacity:"1"}),n.offsetWidth,setTimeout(function(){rr(n,{transition:"all "+o+"ms linear",opacity:"0"}),setTimeout(function(){fe.remove(),l()},o)},o)):setTimeout(()=>l(),o)}),fe},isStarted:()=>typeof fe.status=="number",start:()=>{fe.status||fe.set(0);const e=()=>{setTimeout(()=>{fe.status&&(fe.trickle(),e())},fe.settings.trickleSpeed)};return fe.settings.trickle&&e(),fe},done:e=>!e&&!fe.status?fe:fe.inc(.3+.5*Math.random()).set(1),inc:e=>{let t=fe.status;return t?(typeof e!="number"&&(e=(1-t)*Jr(Math.random()*t,.1,.95)),t=Jr(t+e,0,.994),fe.set(t)):fe.start()},trickle:()=>fe.inc(Math.random()*fe.settings.trickleRate),render:e=>{if(fe.isRendered())return document.getElementById("nprogress");Xs(document.documentElement,"nprogress-busy");const t=document.createElement("div");t.id="nprogress",t.innerHTML=fe.settings.template;const n=t.querySelector(fe.settings.barSelector),r=e?"-100":Zs(fe.status||0),o=document.querySelector(fe.settings.parent);return rr(n,{transition:"all 0 linear",transform:"translate3d("+r+"%,0,0)"}),o!==document.body&&Xs(o,"nprogress-custom-parent"),o==null||o.appendChild(t),t},remove:()=>{el(document.documentElement,"nprogress-busy"),el(document.querySelector(fe.settings.parent),"nprogress-custom-parent");const e=document.getElementById("nprogress");e&&kd(e)},isRendered:()=>!!document.getElementById("nprogress")},Jr=(e,t,n)=>en?n:e,Zs=e=>(-1+e)*100,wd=function(){const e=[];function t(){const n=e.shift();n&&n(t)}return function(n){e.push(n),e.length===1&&t()}}(),rr=function(){const e=["Webkit","O","Moz","ms"],t={};function n(l){return l.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,function(i,a){return a.toUpperCase()})}function r(l){const i=document.body.style;if(l in i)return l;let a=e.length;const c=l.charAt(0).toUpperCase()+l.slice(1);let u;for(;a--;)if(u=e[a]+c,u in i)return u;return l}function o(l){return l=n(l),t[l]??(t[l]=r(l))}function s(l,i,a){i=o(i),l.style[i]=a}return function(l,i){for(const a in i){const c=i[a];c!==void 0&&Object.prototype.hasOwnProperty.call(i,a)&&s(l,a,c)}}}(),Ii=(e,t)=>(typeof e=="string"?e:zo(e)).indexOf(" "+t+" ")>=0,Xs=(e,t)=>{const n=zo(e),r=n+t;Ii(n,t)||(e.className=r.substring(1))},el=(e,t)=>{const n=zo(e);if(!Ii(e,t))return;const r=n.replace(" "+t+" "," ");e.className=r.substring(1,r.length-1)},zo=e=>(" "+(e.className||"")+" ").replace(/\s+/gi," "),kd=e=>{e&&e.parentNode&&e.parentNode.removeChild(e)};const xd=()=>{Je(()=>{const e=Jt(),t=new Set;t.add(e.currentRoute.value.path),e.beforeEach(n=>{t.has(n.path)||fe.start()}),e.afterEach(n=>{t.add(n.path),fe.done()})})},Ld=Dt({setup(){xd()}}),Cd=JSON.parse(`{"repo":"SeldomQA/seldom","docsBranch":"vuepress-docs/docs/vpdocs","logo":"/logo.jpeg","navbar":[{"text":"介绍","link":"/introduce"},{"text":"安装","link":"/getting-started/installation"}],"sidebar":["/introduce",{"text":"开始","children":["/getting-started/installation","/getting-started/create_project","/getting-started/quick_start","/getting-started/advanced","/getting-started/data_driver","/getting-started/dependent_func","/getting-started/seldom_cli"]},{"text":"web UI 测试","children":["/web-testing/browser_driver","/web-testing/seldom_api","/web-testing/chaining","/web-testing/page_object","/web-testing/other"]},{"text":"App UI 测试","children":["/app-testing/start","/app-testing/appium_lab","/app-testing/page_object","/app-testing/extensions"]},{"text":"HTTP接口测试","children":["/api-testing/start","/api-testing/assert","/api-testing/api_object","/api-testing/more","/api-testing/api_case","/api-testing/webscocket"]},{"text":"更多能力","children":["/more-ability/db_operation","/more-ability/test_library"]},"/platform/platform","/version/CHANGES"],"editLinks":true,"editLinkText":"在 GitHub 上编辑此页","lastUpdated":"上次更新","locales":{"/":{"selectLanguageName":"English"}},"colorMode":"auto","colorModeSwitch":true,"selectLanguageText":"Languages","selectLanguageAriaLabel":"Select language","sidebarDepth":2,"editLink":true,"lastUpdatedText":"Last Updated","contributors":true,"contributorsText":"Contributors","notFound":["There's nothing here.","How did we get here?","That's a Four-Oh-Four.","Looks like we've got some broken links."],"backToHome":"Take me home","openInNewWindow":"open in new window","toggleColorMode":"toggle color mode","toggleSidebar":"toggle sidebar"}`),Td=me(Cd),$i=()=>Td,Ni=Symbol(""),Sd=()=>{const e=Pe(Ni);if(!e)throw new Error("useThemeLocaleData() is called without provider.");return e},Pd=(e,t)=>{const{locales:n,...r}=e;return{...r,...n==null?void 0:n[t]}},Ad=Dt({enhance({app:e}){const t=$i(),n=e._context.provides[Do],r=F(()=>Pd(t.value,n.value));e.provide(Ni,r),Object.defineProperties(e.config.globalProperties,{$theme:{get(){return t.value}},$themeLocale:{get(){return r.value}}})}}),Od=ue({__name:"Badge",props:{type:{type:String,required:!1,default:"tip"},text:{type:String,required:!1,default:""},vertical:{type:String,required:!1,default:void 0}},setup(e){return(t,n)=>(j(),X("span",{class:qe(["badge",e.type]),style:jn({verticalAlign:e.vertical})},[Ee(t.$slots,"default",{},()=>[Nt(Re(e.text),1)])],6))}}),Le=(e,t)=>{const n=e.__vccOpts||e;for(const[r,o]of t)n[r]=o;return n},Rd=Le(Od,[["__file","Badge.vue"]]),Id=ue({name:"CodeGroup",slots:Object,setup(e,{slots:t}){const n=me(-1),r=me([]),o=(i=n.value)=>{i{i>0?n.value=i-1:n.value=r.value.length-1,r.value[n.value].focus()},l=(i,a)=>{i.key===" "||i.key==="Enter"?(i.preventDefault(),n.value=a):i.key==="ArrowRight"?(i.preventDefault(),o(a)):i.key==="ArrowLeft"&&(i.preventDefault(),s(a))};return()=>{var a;const i=(((a=t.default)==null?void 0:a.call(t))||[]).filter(c=>c.type.name==="CodeGroupItem").map(c=>(c.props===null&&(c.props={}),c));return i.length===0?null:(n.value<0||n.value>i.length-1?(n.value=i.findIndex(c=>c.props.active===""||c.props.active===!0),n.value===-1&&(n.value=0)):i.forEach((c,u)=>{c.props.active=u===n.value}),ae("div",{class:"code-group"},[ae("div",{class:"code-group__nav"},ae("ul",{class:"code-group__ul"},i.map((c,u)=>{const f=u===n.value;return ae("li",{class:"code-group__li"},ae("button",{ref:h=>{h&&(r.value[u]=h)},class:{"code-group__nav-tab":!0,"code-group__nav-tab-active":f},ariaPressed:f,ariaExpanded:f,onClick:()=>n.value=u,onKeydown:h=>l(h,u)},c.props.title))}))),i]))}}}),$d=["aria-selected"],Nd=ue({name:"CodeGroupItem"}),Dd=ue({...Nd,props:{title:{type:String,required:!0},active:{type:Boolean,required:!1,default:!1}},setup(e){return(t,n)=>(j(),X("div",{class:qe(["code-group-item",{"code-group-item__active":e.active}]),"aria-selected":e.active},[Ee(t.$slots,"default")],10,$d))}}),Md=Le(Dd,[["__file","CodeGroupItem.vue"]]);function Di(e){return yl()?(ma(e),!0):!1}function Mn(e){return typeof e=="function"?e():ee(e)}const Hd=typeof window<"u",Mi=()=>{};function Fd(e,t){function n(...r){return new Promise((o,s)=>{Promise.resolve(e(()=>t.apply(this,r),{fn:t,thisArg:this,args:r})).then(o).catch(s)})}return n}const Hi=e=>e();function jd(e=Hi){const t=me(!0);function n(){t.value=!1}function r(){t.value=!0}const o=(...s)=>{t.value&&e(...s)};return{isActive:Bn(t),pause:n,resume:r,eventFilter:o}}function Bd(...e){if(e.length!==1)return Ja(...e);const t=e[0];return typeof t=="function"?Bn(Ka(()=>({get:t,set:Mi}))):me(t)}function zd(e=!1,t={}){const{truthyValue:n=!0,falsyValue:r=!1}=t,o=Ie(e),s=me(e);function l(i){if(arguments.length)return s.value=i,s.value;{const a=Mn(n);return s.value=s.value===a?Mn(r):a,s.value}}return o?l:[s,l]}var tl=Object.getOwnPropertySymbols,Vd=Object.prototype.hasOwnProperty,Ud=Object.prototype.propertyIsEnumerable,qd=(e,t)=>{var n={};for(var r in e)Vd.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(e!=null&&tl)for(var r of tl(e))t.indexOf(r)<0&&Ud.call(e,r)&&(n[r]=e[r]);return n};function Wd(e,t,n={}){const r=n,{eventFilter:o=Hi}=r,s=qd(r,["eventFilter"]);return st(e,Fd(o,t),s)}var Kd=Object.defineProperty,Gd=Object.defineProperties,Yd=Object.getOwnPropertyDescriptors,Er=Object.getOwnPropertySymbols,Fi=Object.prototype.hasOwnProperty,ji=Object.prototype.propertyIsEnumerable,nl=(e,t,n)=>t in e?Kd(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,Jd=(e,t)=>{for(var n in t||(t={}))Fi.call(t,n)&&nl(e,n,t[n]);if(Er)for(var n of Er(t))ji.call(t,n)&&nl(e,n,t[n]);return e},Qd=(e,t)=>Gd(e,Yd(t)),Zd=(e,t)=>{var n={};for(var r in e)Fi.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(e!=null&&Er)for(var r of Er(e))t.indexOf(r)<0&&ji.call(e,r)&&(n[r]=e[r]);return n};function Xd(e,t,n={}){const r=n,{eventFilter:o}=r,s=Zd(r,["eventFilter"]),{eventFilter:l,pause:i,resume:a,isActive:c}=jd(o);return{stop:Wd(e,t,Qd(Jd({},s),{eventFilter:l})),pause:i,resume:a,isActive:c}}function eh(e){var t;const n=Mn(e);return(t=n==null?void 0:n.$el)!=null?t:n}const wr=Hd?window:void 0;function rl(...e){let t,n,r,o;if(typeof e[0]=="string"||Array.isArray(e[0])?([n,r,o]=e,t=wr):[t,n,r,o]=e,!t)return Mi;Array.isArray(n)||(n=[n]),Array.isArray(r)||(r=[r]);const s=[],l=()=>{s.forEach(u=>u()),s.length=0},i=(u,f,h,g)=>(u.addEventListener(f,h,g),()=>u.removeEventListener(f,h,g)),a=st(()=>[eh(t),Mn(o)],([u,f])=>{l(),u&&s.push(...n.flatMap(h=>r.map(g=>i(u,h,g,f))))},{immediate:!0,flush:"post"}),c=()=>{a(),l()};return Di(c),c}function th(){const e=me(!1);return li()&&Je(()=>{e.value=!0}),e}function nh(e){const t=th();return F(()=>(t.value,!!e()))}function rh(e,t={}){const{window:n=wr}=t,r=nh(()=>n&&"matchMedia"in n&&typeof n.matchMedia=="function");let o;const s=me(!1),l=()=>{o&&("removeEventListener"in o?o.removeEventListener("change",i):o.removeListener(i))},i=()=>{r.value&&(l(),o=n.matchMedia(Bd(e).value),s.value=!!(o!=null&&o.matches),o&&("addEventListener"in o?o.addEventListener("change",i):o.addListener(i)))};return uc(i),Di(()=>l()),s}const or=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{},sr="__vueuse_ssr_handlers__",oh=sh();function sh(){return sr in or||(or[sr]=or[sr]||{}),or[sr]}function lh(e,t){return oh[e]||t}function ih(e){return e==null?"any":e instanceof Set?"set":e instanceof Map?"map":e instanceof Date?"date":typeof e=="boolean"?"boolean":typeof e=="string"?"string":typeof e=="object"?"object":Number.isNaN(e)?"any":"number"}var ah=Object.defineProperty,ol=Object.getOwnPropertySymbols,ch=Object.prototype.hasOwnProperty,uh=Object.prototype.propertyIsEnumerable,sl=(e,t,n)=>t in e?ah(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,ll=(e,t)=>{for(var n in t||(t={}))ch.call(t,n)&&sl(e,n,t[n]);if(ol)for(var n of ol(t))uh.call(t,n)&&sl(e,n,t[n]);return e};const fh={boolean:{read:e=>e==="true",write:e=>String(e)},object:{read:e=>JSON.parse(e),write:e=>JSON.stringify(e)},number:{read:e=>Number.parseFloat(e),write:e=>String(e)},any:{read:e=>e,write:e=>String(e)},string:{read:e=>e,write:e=>String(e)},map:{read:e=>new Map(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e.entries()))},set:{read:e=>new Set(JSON.parse(e)),write:e=>JSON.stringify(Array.from(e))},date:{read:e=>new Date(e),write:e=>e.toISOString()}},il="vueuse-storage";function dh(e,t,n,r={}){var o;const{flush:s="pre",deep:l=!0,listenToStorageChanges:i=!0,writeDefaults:a=!0,mergeDefaults:c=!1,shallow:u,window:f=wr,eventFilter:h,onError:g=m=>{console.error(m)}}=r,y=(u?Il:me)(t);if(!n)try{n=lh("getDefaultStorage",()=>{var m;return(m=wr)==null?void 0:m.localStorage})()}catch(m){g(m)}if(!n)return y;const w=Mn(t),C=ih(w),v=(o=r.serializer)!=null?o:fh[C],{pause:b,resume:P}=Xd(y,()=>S(y.value),{flush:s,deep:l,eventFilter:h});return f&&i&&(rl(f,"storage",$),rl(f,il,Z)),$(),y;function S(m){try{if(m==null)n.removeItem(e);else{const z=v.write(m),M=n.getItem(e);M!==z&&(n.setItem(e,z),f&&f.dispatchEvent(new CustomEvent(il,{detail:{key:e,oldValue:M,newValue:z,storageArea:n}})))}}catch(z){g(z)}}function U(m){const z=m?m.newValue:n.getItem(e);if(z==null)return a&&w!==null&&n.setItem(e,v.write(w)),w;if(!m&&c){const M=v.read(z);return typeof c=="function"?c(M,w):C==="object"&&!Array.isArray(M)?ll(ll({},w),M):M}else return typeof z!="string"?z:v.read(z)}function Z(m){$(m.detail)}function $(m){if(!(m&&m.storageArea!==n)){if(m&&m.key==null){y.value=w;return}if(!(m&&m.key!==e)){b();try{y.value=U(m)}catch(z){g(z)}finally{m?Sr(P):P()}}}}}function hh(e){return rh("(prefers-color-scheme: dark)",e)}const ph=()=>$i(),ze=()=>Sd(),Bi=Symbol(""),Vo=()=>{const e=Pe(Bi);if(!e)throw new Error("useDarkMode() is called without provider.");return e},mh=()=>{const e=ze(),t=hh(),n=dh("vuepress-color-scheme",e.value.colorMode),r=F({get(){return e.value.colorModeSwitch?n.value==="auto"?t.value:n.value==="dark":e.value.colorMode==="dark"},set(o){o===t.value?n.value="auto":n.value=o?"dark":"light"}});Wt(Bi,r),gh(r)},gh=e=>{const t=(n=e.value)=>{const r=window==null?void 0:window.document.querySelector("html");r==null||r.classList.toggle("dark",n)};Je(()=>{st(e,t,{immediate:!0})}),Rr(()=>t())},zi=(...e)=>{const n=Jt().resolve(...e),r=n.matched[n.matched.length-1];if(!(r!=null&&r.redirect))return n;const{redirect:o}=r,s=oe(o)?o(n):o,l=ge(s)?{path:s}:s;return zi({hash:n.hash,query:n.query,params:n.params,...l})},Uo=e=>{const t=zi(encodeURI(e));return{text:t.meta.title||e,link:t.name==="404"?e:t.fullPath}};let Qr=null,wn=null;const vh={wait:()=>Qr,pending:()=>{Qr=new Promise(e=>wn=e)},resolve:()=>{wn==null||wn(),Qr=null,wn=null}},Vi=()=>vh,Ui=Symbol("sidebarItems"),qo=()=>{const e=Pe(Ui);if(!e)throw new Error("useSidebarItems() is called without provider.");return e},_h=()=>{const e=ze(),t=gt(),n=F(()=>bh(t.value,e.value));Wt(Ui,n)},bh=(e,t)=>{const n=e.sidebar??t.sidebar??"auto",r=e.sidebarDepth??t.sidebarDepth??2;return e.home||n===!1?[]:n==="auto"?Eh(r):Q(n)?qi(n,r):No(n)?wh(n,r):[]},yh=(e,t)=>({text:e.title,link:e.link,children:Wo(e.children,t)}),Wo=(e,t)=>t>0?e.map(n=>yh(n,t-1)):[],Eh=e=>{const t=Gt();return[{text:t.value.title,children:Wo(t.value.headers,e)}]},qi=(e,t)=>{const n=Qt(),r=Gt(),o=s=>{var i;let l;if(ge(s)?l=Uo(s):l=s,l.children)return{...l,children:l.children.map(a=>o(a))};if(l.link===n.path){const a=((i=r.value.headers[0])==null?void 0:i.level)===1?r.value.headers[0].children:r.value.headers;return{...l,children:Wo(a,t)}}return l};return e.map(s=>o(s))},wh=(e,t)=>{const n=Qt(),r=di(e,n.path),o=e[r]??[];return qi(o,t)},kh="719px",xh={mobile:kh};var Hn;(function(e){e.MOBILE="mobile"})(Hn||(Hn={}));var hl;const Lh={[Hn.MOBILE]:Number.parseInt((hl=xh.mobile)==null?void 0:hl.replace("px",""),10)},Wi=(e,t)=>{const n=Lh[e];Number.isInteger(n)&&Je(()=>{t(n),window.addEventListener("resize",()=>t(n),!1),window.addEventListener("orientationchange",()=>t(n),!1)})},Ch={},Th={class:"theme-default-content"};function Sh(e,t){const n=bt("Content");return j(),X("div",Th,[te(n)])}const Ph=Le(Ch,[["render",Sh],["__file","HomeContent.vue"]]),Ah={key:0,class:"features"},Oh=ue({__name:"HomeFeatures",setup(e){const t=gt(),n=F(()=>Q(t.value.features)?t.value.features:[]);return(r,o)=>n.value.length?(j(),X("div",Ah,[(j(!0),X(we,null,It(n.value,s=>(j(),X("div",{key:s.title,class:"feature"},[he("h2",null,Re(s.title),1),he("p",null,Re(s.details),1)]))),128))])):Ce("v-if",!0)}}),Rh=Le(Oh,[["__file","HomeFeatures.vue"]]),Ih=["innerHTML"],$h=["textContent"],Nh=ue({__name:"HomeFooter",setup(e){const t=gt(),n=F(()=>t.value.footer),r=F(()=>t.value.footerHtml);return(o,s)=>n.value?(j(),X(we,{key:0},[Ce(" eslint-disable-next-line vue/no-v-html "),r.value?(j(),X("div",{key:0,class:"footer",innerHTML:n.value},null,8,Ih)):(j(),X("div",{key:1,class:"footer",textContent:Re(n.value)},null,8,$h))],64)):Ce("v-if",!0)}}),Dh=Le(Nh,[["__file","HomeFooter.vue"]]),Mh=["href","rel","target","aria-label"],Hh=ue({inheritAttrs:!1}),Fh=ue({...Hh,__name:"AutoLink",props:{item:{type:Object,required:!0}},setup(e){const t=e,n=Qt(),r=bi(),{item:o}=Tr(t),s=F(()=>Wn(o.value.link)),l=F(()=>Fu(o.value.link)||ju(o.value.link)),i=F(()=>{if(!l.value){if(o.value.target)return o.value.target;if(s.value)return"_blank"}}),a=F(()=>i.value==="_blank"),c=F(()=>!s.value&&!l.value&&!a.value),u=F(()=>{if(!l.value){if(o.value.rel)return o.value.rel;if(a.value)return"noopener noreferrer"}}),f=F(()=>o.value.ariaLabel||o.value.text),h=F(()=>{const w=Object.keys(r.value.locales);return w.length?!w.some(C=>C===o.value.link):o.value.link!=="/"}),g=F(()=>h.value?n.path.startsWith(o.value.link):!1),y=F(()=>c.value?o.value.activeMatch?new RegExp(o.value.activeMatch).test(n.path):g.value:!1);return(w,C)=>{const v=bt("RouterLink"),b=bt("AutoLinkExternalIcon");return c.value?(j(),Se(v,co({key:0,class:{"router-link-active":y.value},to:ee(o).link,"aria-label":f.value},w.$attrs),{default:De(()=>[Ee(w.$slots,"before"),Nt(" "+Re(ee(o).text)+" ",1),Ee(w.$slots,"after")]),_:3},16,["class","to","aria-label"])):(j(),X("a",co({key:1,class:"external-link",href:ee(o).link,rel:u.value,target:i.value,"aria-label":f.value},w.$attrs),[Ee(w.$slots,"before"),Nt(" "+Re(ee(o).text)+" ",1),a.value?(j(),Se(b,{key:0})):Ce("v-if",!0),Ee(w.$slots,"after")],16,Mh))}}}),vt=Le(Fh,[["__file","AutoLink.vue"]]),jh={class:"hero"},Bh={key:0,id:"main-title"},zh={key:1,class:"description"},Vh={key:2,class:"actions"},Uh=ue({__name:"HomeHero",setup(e){const t=gt(),n=Mo(),r=Vo(),o=F(()=>r.value&&t.value.heroImageDark!==void 0?t.value.heroImageDark:t.value.heroImage),s=F(()=>t.value.heroAlt||i.value||"hero"),l=F(()=>t.value.heroHeight||280),i=F(()=>t.value.heroText===null?null:t.value.heroText||n.value.title||"Hello"),a=F(()=>t.value.tagline===null?null:t.value.tagline||n.value.description||"Welcome to your VuePress site"),c=F(()=>Q(t.value.actions)?t.value.actions.map(({text:f,link:h,type:g="primary"})=>({text:f,link:h,type:g})):[]),u=()=>{if(!o.value)return null;const f=ae("img",{src:Fo(o.value),alt:s.value,height:l.value});return t.value.heroImageDark===void 0?f:ae(Ho,()=>f)};return(f,h)=>(j(),X("header",jh,[te(u),i.value?(j(),X("h1",Bh,Re(i.value),1)):Ce("v-if",!0),a.value?(j(),X("p",zh,Re(a.value),1)):Ce("v-if",!0),c.value.length?(j(),X("p",Vh,[(j(!0),X(we,null,It(c.value,g=>(j(),Se(vt,{key:g.text,class:qe(["action-button",[g.type]]),item:g},null,8,["class","item"]))),128))])):Ce("v-if",!0)]))}}),qh=Le(Uh,[["__file","HomeHero.vue"]]),Wh={class:"home"},Kh=ue({__name:"Home",setup(e){return(t,n)=>(j(),X("main",Wh,[te(qh),te(Rh),te(Ph),te(Dh)]))}}),Gh=Le(Kh,[["__file","Home.vue"]]),Yh=ue({__name:"NavbarBrand",setup(e){const t=Kn(),n=Mo(),r=ze(),o=Vo(),s=F(()=>r.value.home||t.value),l=F(()=>n.value.title),i=F(()=>o.value&&r.value.logoDark!==void 0?r.value.logoDark:r.value.logo),a=()=>{if(!i.value)return null;const c=ae("img",{class:"logo",src:Fo(i.value),alt:l.value});return r.value.logoDark===void 0?c:ae(Ho,()=>c)};return(c,u)=>{const f=bt("RouterLink");return j(),Se(f,{to:s.value},{default:De(()=>[te(a),l.value?(j(),X("span",{key:0,class:qe(["site-name",{"can-hide":i.value}])},Re(l.value),3)):Ce("v-if",!0)]),_:1},8,["to"])}}}),Jh=Le(Yh,[["__file","NavbarBrand.vue"]]),Qh=ue({__name:"DropdownTransition",setup(e){const t=r=>{r.style.height=r.scrollHeight+"px"},n=r=>{r.style.height=""};return(r,o)=>(j(),Se(qn,{name:"dropdown",onEnter:t,onAfterEnter:n,onBeforeLeave:t},{default:De(()=>[Ee(r.$slots,"default")]),_:3}))}}),Ki=Le(Qh,[["__file","DropdownTransition.vue"]]),Zh=["aria-label"],Xh={class:"title"},ep=he("span",{class:"arrow down"},null,-1),tp=["aria-label"],np={class:"title"},rp={class:"navbar-dropdown"},op={class:"navbar-dropdown-subtitle"},sp={key:1},lp={class:"navbar-dropdown-subitem-wrapper"},ip=ue({__name:"NavbarDropdown",props:{item:{type:Object,required:!0}},setup(e){const t=e,{item:n}=Tr(t),r=F(()=>n.value.ariaLabel||n.value.text),o=me(!1),s=Qt();st(()=>s.path,()=>{o.value=!1});const l=a=>{a.detail===0?o.value=!o.value:o.value=!1},i=(a,c)=>c[c.length-1]===a;return(a,c)=>(j(),X("div",{class:qe(["navbar-dropdown-wrapper",{open:o.value}])},[he("button",{class:"navbar-dropdown-title",type:"button","aria-label":r.value,onClick:l},[he("span",Xh,Re(ee(n).text),1),ep],8,Zh),he("button",{class:"navbar-dropdown-title-mobile",type:"button","aria-label":r.value,onClick:c[0]||(c[0]=u=>o.value=!o.value)},[he("span",np,Re(ee(n).text),1),he("span",{class:qe(["arrow",o.value?"down":"right"])},null,2)],8,tp),te(Ki,null,{default:De(()=>[pr(he("ul",rp,[(j(!0),X(we,null,It(ee(n).children,u=>(j(),X("li",{key:u.text,class:"navbar-dropdown-item"},[u.children?(j(),X(we,{key:0},[he("h4",op,[u.link?(j(),Se(vt,{key:0,item:u,onFocusout:f=>i(u,ee(n).children)&&u.children.length===0&&(o.value=!1)},null,8,["item","onFocusout"])):(j(),X("span",sp,Re(u.text),1))]),he("ul",lp,[(j(!0),X(we,null,It(u.children,f=>(j(),X("li",{key:f.link,class:"navbar-dropdown-subitem"},[te(vt,{item:f,onFocusout:h=>i(f,u.children)&&i(u,ee(n).children)&&(o.value=!1)},null,8,["item","onFocusout"])]))),128))])],64)):(j(),Se(vt,{key:1,item:u,onFocusout:f=>i(u,ee(n).children)&&(o.value=!1)},null,8,["item","onFocusout"]))]))),128))],512),[[br,o.value]])]),_:1})],2))}}),ap=Le(ip,[["__file","NavbarDropdown.vue"]]),al=e=>decodeURI(e).replace(/#.*$/,"").replace(/(index)?\.(md|html)$/,""),cp=(e,t)=>{if(t.hash===e)return!0;const n=al(t.path),r=al(e);return n===r},Gi=(e,t)=>e.link&&cp(e.link,t)?!0:e.children?e.children.some(n=>Gi(n,t)):!1,Yi=e=>!Wn(e)||/github\.com/.test(e)?"GitHub":/bitbucket\.org/.test(e)?"Bitbucket":/gitlab\.com/.test(e)?"GitLab":/gitee\.com/.test(e)?"Gitee":null,up={GitHub:":repo/edit/:branch/:path",GitLab:":repo/-/edit/:branch/:path",Gitee:":repo/edit/:branch/:path",Bitbucket:":repo/src/:branch/:path?mode=edit&spa=0&at=:branch&fileviewer=file-view-default"},fp=({docsRepo:e,editLinkPattern:t})=>{if(t)return t;const n=Yi(e);return n!==null?up[n]:null},dp=({docsRepo:e,docsBranch:t,docsDir:n,filePathRelative:r,editLinkPattern:o})=>{if(!r)return null;const s=fp({docsRepo:e,editLinkPattern:o});return s?s.replace(/:repo/,Wn(e)?e:`https://github.com/${e}`).replace(/:branch/,t).replace(/:path/,fi(`${ui(n)}/${r}`)):null},hp={key:0,class:"navbar-items"},pp=ue({__name:"NavbarItems",setup(e){const t=()=>{const u=Jt(),f=Kn(),h=bi(),g=Mo(),y=ph(),w=ze();return F(()=>{const C=Object.keys(h.value.locales);if(C.length<2)return[];const v=u.currentRoute.value.path,b=u.currentRoute.value.fullPath;return[{text:`${w.value.selectLanguageText}`,ariaLabel:`${w.value.selectLanguageAriaLabel??w.value.selectLanguageText}`,children:C.map(S=>{var M,G;const U=((M=h.value.locales)==null?void 0:M[S])??{},Z=((G=y.value.locales)==null?void 0:G[S])??{},$=`${U.lang}`,m=Z.selectLanguageName??$;let z;if($===g.value.lang)z=b;else{const L=v.replace(f.value,S);u.getRoutes().some(R=>R.path===L)?z=b.replace(v,L):z=Z.home??S}return{text:m,link:z}})}]})},n=()=>{const u=ze(),f=F(()=>u.value.repo),h=F(()=>f.value?Yi(f.value):null),g=F(()=>f.value&&!Wn(f.value)?`https://github.com/${f.value}`:f.value),y=F(()=>g.value?u.value.repoLabel?u.value.repoLabel:h.value===null?"Source":h.value:null);return F(()=>!g.value||!y.value?[]:[{text:y.value,link:g.value}])},r=u=>ge(u)?Uo(u):u.children?{...u,children:u.children.map(r)}:u,o=()=>{const u=ze();return F(()=>(u.value.navbar||[]).map(r))},s=me(!1),l=o(),i=t(),a=n(),c=F(()=>[...l.value,...i.value,...a.value]);return Wi(Hn.MOBILE,u=>{window.innerWidthc.value.length?(j(),X("nav",hp,[(j(!0),X(we,null,It(c.value,h=>(j(),X("div",{key:h.text,class:"navbar-item"},[h.children?(j(),Se(ap,{key:0,item:h,class:qe(s.value?"mobile":"")},null,8,["item","class"])):(j(),Se(vt,{key:1,item:h},null,8,["item"]))]))),128))])):Ce("v-if",!0)}}),Ji=Le(pp,[["__file","NavbarItems.vue"]]),mp=["title"],gp={class:"icon",focusable:"false",viewBox:"0 0 32 32"},vp=Gc('',9),_p=[vp],bp={class:"icon",focusable:"false",viewBox:"0 0 32 32"},yp=he("path",{d:"M13.502 5.414a15.075 15.075 0 0 0 11.594 18.194a11.113 11.113 0 0 1-7.975 3.39c-.138 0-.278.005-.418 0a11.094 11.094 0 0 1-3.2-21.584M14.98 3a1.002 1.002 0 0 0-.175.016a13.096 13.096 0 0 0 1.825 25.981c.164.006.328 0 .49 0a13.072 13.072 0 0 0 10.703-5.555a1.01 1.01 0 0 0-.783-1.565A13.08 13.08 0 0 1 15.89 4.38A1.015 1.015 0 0 0 14.98 3z",fill:"currentColor"},null,-1),Ep=[yp],wp=ue({__name:"ToggleColorModeButton",setup(e){const t=ze(),n=Vo(),r=()=>{n.value=!n.value};return(o,s)=>(j(),X("button",{class:"toggle-color-mode-button",title:ee(t).toggleColorMode,onClick:r},[pr((j(),X("svg",gp,_p,512)),[[br,!ee(n)]]),pr((j(),X("svg",bp,Ep,512)),[[br,ee(n)]])],8,mp))}}),kp=Le(wp,[["__file","ToggleColorModeButton.vue"]]),xp=["title"],Lp=he("div",{class:"icon","aria-hidden":"true"},[he("span"),he("span"),he("span")],-1),Cp=[Lp],Tp=ue({__name:"ToggleSidebarButton",emits:["toggle"],setup(e){const t=ze();return(n,r)=>(j(),X("div",{class:"toggle-sidebar-button",title:ee(t).toggleSidebar,"aria-expanded":"false",role:"button",tabindex:"0",onClick:r[0]||(r[0]=o=>n.$emit("toggle"))},Cp,8,xp))}}),Sp=Le(Tp,[["__file","ToggleSidebarButton.vue"]]),Pp=ue({__name:"Navbar",emits:["toggle-sidebar"],setup(e){const t=ze(),n=me(null),r=me(null),o=me(0),s=F(()=>o.value?{maxWidth:o.value+"px"}:{});Wi(Hn.MOBILE,i=>{var c;const a=l(n.value,"paddingLeft")+l(n.value,"paddingRight");window.innerWidth{const c=bt("NavbarSearch");return j(),X("header",{ref_key:"navbar",ref:n,class:"navbar"},[te(Sp,{onToggle:a[0]||(a[0]=u=>i.$emit("toggle-sidebar"))}),he("span",{ref_key:"navbarBrand",ref:r},[te(Jh)],512),he("div",{class:"navbar-items-wrapper",style:jn(s.value)},[Ee(i.$slots,"before"),te(Ji,{class:"can-hide"}),Ee(i.$slots,"after"),ee(t).colorModeSwitch?(j(),Se(kp,{key:0})):Ce("v-if",!0),te(c)],4)],512)}}}),Ap=Le(Pp,[["__file","Navbar.vue"]]),Op={class:"page-meta"},Rp={key:0,class:"meta-item edit-link"},Ip={key:1,class:"meta-item last-updated"},$p={class:"meta-item-label"},Np={class:"meta-item-info"},Dp={key:2,class:"meta-item contributors"},Mp={class:"meta-item-label"},Hp={class:"meta-item-info"},Fp=["title"],jp=ue({__name:"PageMeta",setup(e){const t=()=>{const a=ze(),c=Gt(),u=gt();return F(()=>{if(!(u.value.editLink??a.value.editLink??!0))return null;const{repo:h,docsRepo:g=h,docsBranch:y="main",docsDir:w="",editLinkText:C}=a.value;if(!g)return null;const v=dp({docsRepo:g,docsBranch:y,docsDir:w,filePathRelative:c.value.filePathRelative,editLinkPattern:u.value.editLinkPattern??a.value.editLinkPattern});return v?{text:C??"Edit this page",link:v}:null})},n=()=>{const a=ze(),c=Gt(),u=gt();return F(()=>{var g,y;return!(u.value.lastUpdated??a.value.lastUpdated??!0)||!((g=c.value.git)!=null&&g.updatedTime)?null:new Date((y=c.value.git)==null?void 0:y.updatedTime).toLocaleString()})},r=()=>{const a=ze(),c=Gt(),u=gt();return F(()=>{var h;return u.value.contributors??a.value.contributors??!0?((h=c.value.git)==null?void 0:h.contributors)??null:null})},o=ze(),s=t(),l=n(),i=r();return(a,c)=>{const u=bt("ClientOnly");return j(),X("footer",Op,[ee(s)?(j(),X("div",Rp,[te(vt,{class:"meta-item-label",item:ee(s)},null,8,["item"])])):Ce("v-if",!0),ee(l)?(j(),X("div",Ip,[he("span",$p,Re(ee(o).lastUpdatedText)+": ",1),te(u,null,{default:De(()=>[he("span",Np,Re(ee(l)),1)]),_:1})])):Ce("v-if",!0),ee(i)&&ee(i).length?(j(),X("div",Dp,[he("span",Mp,Re(ee(o).contributorsText)+": ",1),he("span",Hp,[(j(!0),X(we,null,It(ee(i),(f,h)=>(j(),X(we,{key:h},[he("span",{class:"contributor",title:`email: ${f.email}`},Re(f.name),9,Fp),h!==ee(i).length-1?(j(),X(we,{key:0},[Nt(", ")],64)):Ce("v-if",!0)],64))),128))])])):Ce("v-if",!0)])}}}),Bp=Le(jp,[["__file","PageMeta.vue"]]),zp={key:0,class:"page-nav"},Vp={class:"inner"},Up={key:0,class:"prev"},qp={key:1,class:"next"},Wp=ue({__name:"PageNav",setup(e){const t=a=>a===!1?null:ge(a)?Uo(a):No(a)?a:!1,n=(a,c,u)=>{const f=a.findIndex(h=>h.link===c);if(f!==-1){const h=a[f+u];return h!=null&&h.link?h:null}for(const h of a)if(h.children){const g=n(h.children,c,u);if(g)return g}return null},r=gt(),o=qo(),s=Qt(),l=F(()=>{const a=t(r.value.prev);return a!==!1?a:n(o.value,s.path,-1)}),i=F(()=>{const a=t(r.value.next);return a!==!1?a:n(o.value,s.path,1)});return(a,c)=>l.value||i.value?(j(),X("nav",zp,[he("p",Vp,[l.value?(j(),X("span",Up,[te(vt,{item:l.value},null,8,["item"])])):Ce("v-if",!0),i.value?(j(),X("span",qp,[te(vt,{item:i.value},null,8,["item"])])):Ce("v-if",!0)])])):Ce("v-if",!0)}}),Kp=Le(Wp,[["__file","PageNav.vue"]]),Gp={class:"page"},Yp={class:"theme-default-content"},Jp=ue({__name:"Page",setup(e){return(t,n)=>{const r=bt("Content");return j(),X("main",Gp,[Ee(t.$slots,"top"),he("div",Yp,[Ee(t.$slots,"content-top"),te(r),Ee(t.$slots,"content-bottom")]),te(Bp),te(Kp),Ee(t.$slots,"bottom")])}}}),Qp=Le(Jp,[["__file","Page.vue"]]),Zp=["onKeydown"],Xp={class:"sidebar-item-children"},em=ue({__name:"SidebarItem",props:{item:{type:Object,required:!0},depth:{type:Number,required:!1,default:0}},setup(e){const t=e,{item:n,depth:r}=Tr(t),o=Qt(),s=Jt(),l=F(()=>Gi(n.value,o)),i=F(()=>({"sidebar-item":!0,"sidebar-heading":r.value===0,active:l.value,collapsible:n.value.collapsible})),a=F(()=>n.value.collapsible?l.value:!0),[c,u]=zd(a.value),f=g=>{n.value.collapsible&&(g.preventDefault(),u())},h=s.afterEach(g=>{Sr(()=>{c.value=a.value})});return Un(()=>{h()}),(g,y)=>{var C;const w=bt("SidebarItem",!0);return j(),X("li",null,[ee(n).link?(j(),Se(vt,{key:0,class:qe(i.value),item:ee(n)},null,8,["class","item"])):(j(),X("p",{key:1,tabindex:"0",class:qe(i.value),onClick:f,onKeydown:Au(f,["enter"])},[Nt(Re(ee(n).text)+" ",1),ee(n).collapsible?(j(),X("span",{key:0,class:qe(["arrow",ee(c)?"down":"right"])},null,2)):Ce("v-if",!0)],42,Zp)),(C=ee(n).children)!=null&&C.length?(j(),Se(Ki,{key:2},{default:De(()=>[pr(he("ul",Xp,[(j(!0),X(we,null,It(ee(n).children,v=>(j(),Se(w,{key:`${ee(r)}${v.text}${v.link}`,item:v,depth:ee(r)+1},null,8,["item","depth"]))),128))],512),[[br,ee(c)]])]),_:1})):Ce("v-if",!0)])}}}),tm=Le(em,[["__file","SidebarItem.vue"]]),nm={key:0,class:"sidebar-items"},rm=ue({__name:"SidebarItems",setup(e){const t=Qt(),n=qo();return Je(()=>{st(()=>t.hash,r=>{const o=document.querySelector(".sidebar");if(!o)return;const s=document.querySelector(`.sidebar a.sidebar-item[href="${t.path}${r}"]`);if(!s)return;const{top:l,height:i}=o.getBoundingClientRect(),{top:a,height:c}=s.getBoundingClientRect();al+i&&s.scrollIntoView(!1)})}),(r,o)=>ee(n).length?(j(),X("ul",nm,[(j(!0),X(we,null,It(ee(n),s=>(j(),Se(tm,{key:`${s.text}${s.link}`,item:s},null,8,["item"]))),128))])):Ce("v-if",!0)}}),om=Le(rm,[["__file","SidebarItems.vue"]]),sm={class:"sidebar"},lm=ue({__name:"Sidebar",setup(e){return(t,n)=>(j(),X("aside",sm,[te(Ji),Ee(t.$slots,"top"),te(om),Ee(t.$slots,"bottom")]))}}),im=Le(lm,[["__file","Sidebar.vue"]]),am=ue({__name:"Layout",setup(e){const t=Gt(),n=gt(),r=ze(),o=F(()=>n.value.navbar!==!1&&r.value.navbar!==!1),s=qo(),l=me(!1),i=C=>{l.value=typeof C=="boolean"?C:!l.value},a={x:0,y:0},c=C=>{a.x=C.changedTouches[0].clientX,a.y=C.changedTouches[0].clientY},u=C=>{const v=C.changedTouches[0].clientX-a.x,b=C.changedTouches[0].clientY-a.y;Math.abs(v)>Math.abs(b)&&Math.abs(v)>40&&(v>0&&a.x<=80?i(!0):i(!1))},f=F(()=>[{"no-navbar":!o.value,"no-sidebar":!s.value.length,"sidebar-open":l.value},n.value.pageClass]);let h;Je(()=>{h=Jt().afterEach(()=>{i(!1)})}),Rr(()=>{h()});const g=Vi(),y=g.resolve,w=g.pending;return(C,v)=>(j(),X("div",{class:qe(["theme-container",f.value]),onTouchstart:c,onTouchend:u},[Ee(C.$slots,"navbar",{},()=>[o.value?(j(),Se(Ap,{key:0,onToggleSidebar:i},{before:De(()=>[Ee(C.$slots,"navbar-before")]),after:De(()=>[Ee(C.$slots,"navbar-after")]),_:3})):Ce("v-if",!0)]),he("div",{class:"sidebar-mask",onClick:v[0]||(v[0]=b=>i(!1))}),Ee(C.$slots,"sidebar",{},()=>[te(im,null,{top:De(()=>[Ee(C.$slots,"sidebar-top")]),bottom:De(()=>[Ee(C.$slots,"sidebar-bottom")]),_:3})]),Ee(C.$slots,"page",{},()=>[ee(n).home?(j(),Se(Gh,{key:0})):(j(),Se(qn,{key:1,name:"fade-slide-y",mode:"out-in",onBeforeEnter:ee(y),onBeforeLeave:ee(w)},{default:De(()=>[(j(),Se(Qp,{key:ee(t).path},{top:De(()=>[Ee(C.$slots,"page-top")]),"content-top":De(()=>[Ee(C.$slots,"page-content-top")]),"content-bottom":De(()=>[Ee(C.$slots,"page-content-bottom")]),bottom:De(()=>[Ee(C.$slots,"page-bottom")]),_:3}))]),_:3},8,["onBeforeEnter","onBeforeLeave"]))])],34))}}),cm=Le(am,[["__file","Layout.vue"]]),um={class:"theme-container"},fm={class:"page"},dm={class:"theme-default-content"},hm=he("h1",null,"404",-1),pm=ue({__name:"NotFound",setup(e){const t=Kn(),n=ze(),r=n.value.notFound??["Not Found"],o=()=>r[Math.floor(Math.random()*r.length)],s=n.value.home??t.value,l=n.value.backToHome??"Back to home";return(i,a)=>{const c=bt("RouterLink");return j(),X("div",um,[he("main",fm,[he("div",dm,[hm,he("blockquote",null,Re(o()),1),te(c,{to:ee(s)},{default:De(()=>[Nt(Re(ee(l)),1)]),_:1},8,["to"])])])])}}}),mm=Le(pm,[["__file","NotFound.vue"]]);const gm=Dt({enhance({app:e,router:t}){e.component("Badge",Rd),e.component("CodeGroup",Id),e.component("CodeGroupItem",Md),e.component("AutoLinkExternalIcon",()=>{const r=e.component("ExternalLinkIcon");return r?ae(r):null}),e.component("NavbarSearch",()=>{const r=e.component("Docsearch")||e.component("SearchBox");return r?ae(r):null});const n=t.options.scrollBehavior;t.options.scrollBehavior=async(...r)=>(await Vi().wait(),n(...r))},setup(){mh(),_h()},layouts:{Layout:cm,NotFound:mm}}),vm=e=>e instanceof Element?document.activeElement===e&&(["TEXTAREA","SELECT","INPUT"].includes(e.tagName)||e.hasAttribute("contenteditable")):!1,_m=(e,t)=>t.some(n=>{if(ge(n))return n===e.key;const{key:r,ctrl:o=!1,shift:s=!1,alt:l=!1}=n;return r===e.key&&o===e.ctrlKey&&s===e.shiftKey&&l===e.altKey}),bm=/[^\x00-\x7F]/,ym=e=>e.split(/\s+/g).map(t=>t.trim()).filter(t=>!!t),cl=e=>e.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"),ul=(e,t)=>{const n=t.join(" "),r=ym(e);if(bm.test(e))return r.some(l=>n.toLowerCase().indexOf(l)>-1);const o=e.endsWith(" ");return new RegExp(r.map((l,i)=>r.length===i+1&&!o?`(?=.*\\b${cl(l)})`:`(?=.*\\b${cl(l)}\\b)`).join("")+".+","gi").test(n)},Em=({input:e,hotKeys:t})=>{if(t.value.length===0)return;const n=r=>{e.value&&_m(r,t.value)&&!vm(r.target)&&(r.preventDefault(),e.value.focus())};Je(()=>{document.addEventListener("keydown",n)}),Un(()=>{document.removeEventListener("keydown",n)})},wm=[{title:"",headers:[{level:2,title:"☘️Introduction",slug:"☘️introduction",link:"#☘️introduction",children:[]},{level:2,title:"📖使用说明",slug:"📖使用说明",link:"#📖使用说明",children:[{level:3,title:"1. 安装",slug:"_1-安装",link:"#_1-安装",children:[]},{level:3,title:"2. 开发",slug:"_2-开发",link:"#_2-开发",children:[]}]},{level:2,title:"部署",slug:"部署",link:"#部署",children:[]}],path:"/develop.html",pathLocale:"/",extraFields:[]},{title:"介绍",headers:[{level:2,title:"新书推荐",slug:"新书推荐",link:"#新书推荐",children:[]},{level:2,title:"seldom框架",slug:"seldom框架",link:"#seldom框架",children:[{level:3,title:"特点",slug:"特点",link:"#特点",children:[]},{level:3,title:"设计理念",slug:"设计理念",link:"#设计理念",children:[]},{level:3,title:"发展历史",slug:"发展历史",link:"#发展历史",children:[]},{level:3,title:"seldom vs pytest",slug:"seldom-vs-pytest",link:"#seldom-vs-pytest",children:[]}]}],path:"/introduce.html",pathLocale:"/",extraFields:[]},{title:"",headers:[],path:"/",pathLocale:"/",extraFields:[]},{title:"支持Excel测试用例",headers:[{level:3,title:"编写Excel用例",slug:"编写excel用例",link:"#编写excel用例",children:[]},{level:3,title:"运行测试用例",slug:"运行测试用例",link:"#运行测试用例",children:[]}],path:"/api-testing/api_case.html",pathLocale:"/",extraFields:[]},{title:"API Object",headers:[],path:"/api-testing/api_object.html",pathLocale:"/",extraFields:[]},{title:"更强大的断言",headers:[{level:3,title:"assertJSON",slug:"assertjson",link:"#assertjson",children:[]},{level:3,title:"assertPath",slug:"assertpath",link:"#assertpath",children:[]},{level:3,title:"assertSchema",slug:"assertschema",link:"#assertschema",children:[]}],path:"/api-testing/assert.html",pathLocale:"/",extraFields:[]},{title:"更多功能",headers:[{level:3,title:"har to case",slug:"har-to-case",link:"#har-to-case",children:[]},{level:3,title:"swagger to case",slug:"swagger-to-case",link:"#swagger-to-case",children:[]},{level:3,title:"请求转 cURL",slug:"请求转-curl",link:"#请求转-curl",children:[]},{level:3,title:"接口数据依赖",slug:"接口数据依赖",link:"#接口数据依赖",children:[]},{level:3,title:"Session使用",slug:"session使用",link:"#session使用",children:[]},{level:3,title:"提取接口返回数据",slug:"提取接口返回数据",link:"#提取接口返回数据",children:[]},{level:3,title:"genson",slug:"genson",link:"#genson",children:[]},{level:3,title:"mock URL",slug:"mock-url",link:"#mock-url",children:[]},{level:3,title:"@retry装饰器",slug:"retry装饰器",link:"#retry装饰器",children:[]}],path:"/api-testing/more.html",pathLocale:"/",extraFields:[]},{title:"开始使用",headers:[{level:3,title:"前言",slug:"前言",link:"#前言",children:[]},{level:3,title:"Seldom VS Request+unittest",slug:"seldom-vs-request-unittest",link:"#seldom-vs-request-unittest",children:[]},{level:3,title:"运行测试",slug:"运行测试",link:"#运行测试",children:[]}],path:"/api-testing/start.html",pathLocale:"/",extraFields:[]},{title:"WebSocket",headers:[{level:3,title:"WebSocket 生命周期",slug:"websocket-生命周期",link:"#websocket-生命周期",children:[]},{level:3,title:"seldom测试WebSocket",slug:"seldom测试websocket",link:"#seldom测试websocket",children:[]}],path:"/api-testing/webscocket.html",pathLocale:"/",extraFields:[]},{title:"appium API",headers:[{level:2,title:"appium 定位",slug:"appium-定位",link:"#appium-定位",children:[]},{level:2,title:"appium lab",slug:"appium-lab",link:"#appium-lab",children:[]},{level:2,title:"appium driver",slug:"appium-driver",link:"#appium-driver",children:[]}],path:"/app-testing/appium_lab.html",pathLocale:"/",extraFields:[]},{title:"appium 扩展",headers:[{level:2,title:"appium images-plugin",slug:"appium-images-plugin",link:"#appium-images-plugin",children:[]},{level:2,title:"Appium OCR plugin",slug:"appium-ocr-plugin",link:"#appium-ocr-plugin",children:[]}],path:"/app-testing/extensions.html",pathLocale:"/",extraFields:[]},{title:"Page Object",headers:[],path:"/app-testing/page_object.html",pathLocale:"/",extraFields:[]},{title:"app 测试",headers:[{level:2,title:"环境安装",slug:"环境安装",link:"#环境安装",children:[]},{level:2,title:"编写测试",slug:"编写测试",link:"#编写测试",children:[]}],path:"/app-testing/start.html",pathLocale:"/",extraFields:[]},{title:"数据库操作",headers:[{level:3,title:"连接数据库",slug:"连接数据库",link:"#连接数据库",children:[]},{level:3,title:"操作方法",slug:"操作方法",link:"#操作方法",children:[]},{level:2,title:"MongoDB",slug:"mongodb",link:"#mongodb",children:[]}],path:"/more-ability/db_operation.html",pathLocale:"/",extraFields:[]},{title:"支持更多测试库",headers:[{level:3,title:"使用playwright",slug:"使用playwright",link:"#使用playwright",children:[]},{level:3,title:"使用uiautomator2",slug:"使用uiautomator2",link:"#使用uiautomator2",children:[]},{level:3,title:"使用pyAutoGUI",slug:"使用pyautogui",link:"#使用pyautogui",children:[]}],path:"/more-ability/test_library.html",pathLocale:"/",extraFields:[]},{title:"高级用法",headers:[{level:3,title:"fixture",slug:"fixture",link:"#fixture",children:[]},{level:3,title:"跳过测试",slug:"跳过测试",link:"#跳过测试",children:[]},{level:3,title:"重复执行",slug:"重复执行",link:"#重复执行",children:[]},{level:3,title:"随机测试数据",slug:"随机测试数据",link:"#随机测试数据",children:[]},{level:3,title:"用例的依赖",slug:"用例的依赖",link:"#用例的依赖",children:[]},{level:3,title:"用例分类标签",slug:"用例分类标签",link:"#用例分类标签",children:[]},{level:3,title:"发送邮件",slug:"发送邮件",link:"#发送邮件",children:[]},{level:3,title:"发送钉钉",slug:"发送钉钉",link:"#发送钉钉",children:[]},{level:3,title:"seldom日志",slug:"seldom日志",link:"#seldom日志",children:[]},{level:3,title:"缓存 cache",slug:"缓存-cache",link:"#缓存-cache",children:[]}],path:"/getting-started/advanced.html",pathLocale:"/",extraFields:[]},{title:"创建项目",headers:[{level:3,title:"自动生成项目",slug:"自动生成项目",link:"#自动生成项目",children:[]},{level:3,title:"创建测试用例",slug:"创建测试用例",link:"#创建测试用例",children:[]}],path:"/getting-started/create_project.html",pathLocale:"/",extraFields:[]},{title:"数据驱动",headers:[{level:3,title:"@class_data() 方法",slug:"class-data-方法",link:"#class-data-方法",children:[]},{level:3,title:"@data()方法",slug:"data-方法",link:"#data-方法",children:[]},{level:3,title:"@file_data() 方法",slug:"file-data-方法",link:"#file-data-方法",children:[]},{level:3,title:"@api_data()方法",slug:"api-data-方法",link:"#api-data-方法",children:[]},{level:3,title:"使用函数构造数据",slug:"使用函数构造数据",link:"#使用函数构造数据",children:[]},{level:3,title:"支持第三方 ddt 库",slug:"支持第三方-ddt-库",link:"#支持第三方-ddt-库",children:[]}],path:"/getting-started/data_driver.html",pathLocale:"/",extraFields:[]},{title:"方法的依赖",headers:[{level:3,title:"类内部方法调用",slug:"类内部方法调用",link:"#类内部方法调用",children:[]},{level:3,title:"外部类方法依赖",slug:"外部类方法依赖",link:"#外部类方法依赖",children:[]},{level:3,title:"多重方法依赖",slug:"多重方法依赖",link:"#多重方法依赖",children:[]},{level:3,title:"参数化使用",slug:"参数化使用",link:"#参数化使用",children:[]}],path:"/getting-started/dependent_func.html",pathLocale:"/",extraFields:[]},{title:"Installation",headers:[],path:"/getting-started/installation.html",pathLocale:"/",extraFields:[]},{title:"快速开始",headers:[{level:3,title:"基本规范",slug:"基本规范",link:"#基本规范",children:[]},{level:3,title:"main() 方法",slug:"main-方法",link:"#main-方法",children:[]},{level:3,title:"confrun.py 配置文件",slug:"confrun-py-配置文件",link:"#confrun-py-配置文件",children:[]},{level:3,title:"运行测试",slug:"运行测试",link:"#运行测试",children:[]},{level:3,title:"失败重跑",slug:"失败重跑",link:"#失败重跑",children:[]},{level:3,title:"测试报告",slug:"测试报告",link:"#测试报告",children:[]},{level:3,title:"多线程运行",slug:"多线程运行",link:"#多线程运行",children:[]}],path:"/getting-started/quick_start.html",pathLocale:"/",extraFields:[]},{title:"seldom CLI",headers:[{level:2,title:"seldom 帮助",slug:"seldom-帮助",link:"#seldom-帮助",children:[]},{level:2,title:"seldom 使用",slug:"seldom-使用",link:"#seldom-使用",children:[{level:3,title:"创建项目",slug:"创建项目",link:"#创建项目",children:[]},{level:3,title:"生成接口自动化用例",slug:"生成接口自动化用例",link:"#生成接口自动化用例",children:[]},{level:3,title:"运行测试目录&文件",slug:"运行测试目录-文件",link:"#运行测试目录-文件",children:[]},{level:3,title:"运行文件&类&方法",slug:"运行文件-类-方法",link:"#运行文件-类-方法",children:[]},{level:3,title:"调试模式",slug:"调试模式",link:"#调试模式",children:[]},{level:3,title:"运行浏览器",slug:"运行浏览器",link:"#运行浏览器",children:[]},{level:3,title:"运行URL",slug:"运行url",link:"#运行url",children:[]},{level:3,title:"测试报告",slug:"测试报告",link:"#测试报告",children:[]},{level:3,title:"失败/错误重跑次数",slug:"失败-错误重跑次数",link:"#失败-错误重跑次数",children:[]},{level:3,title:"数据驱动运行环境",slug:"数据驱动运行环境",link:"#数据驱动运行环境",children:[]},{level:3,title:"收集测试用例",slug:"收集测试用例",link:"#收集测试用例",children:[]},{level:3,title:"运行收集测试用例",slug:"运行收集测试用例",link:"#运行收集测试用例",children:[]},{level:3,title:"清除所有缓存",slug:"清除所有缓存",link:"#清除所有缓存",children:[]},{level:3,title:"执行 API(excel文件)测试用例",slug:"执行-api-excel文件-测试用例",link:"#执行-api-excel文件-测试用例",children:[]}]}],path:"/getting-started/seldom_cli.html",pathLocale:"/",extraFields:[]},{title:"平台化支持",headers:[{level:3,title:"获取用例信息",slug:"获取用例信息",link:"#获取用例信息",children:[]},{level:3,title:"执行用例信息",slug:"执行用例信息",link:"#执行用例信息",children:[]},{level:3,title:"相关项目",slug:"相关项目",link:"#相关项目",children:[]}],path:"/platform/platform.html",pathLocale:"/",extraFields:[]},{title:"版本更新",headers:[{level:3,title:"seldom 3.x",slug:"seldom-3-x",link:"#seldom-3-x",children:[]},{level:3,title:"seldom 2.x",slug:"seldom-2-x",link:"#seldom-2-x",children:[]},{level:3,title:"seldom 1.x",slug:"seldom-1-x",link:"#seldom-1-x",children:[]},{level:3,title:"seldom 0.x",slug:"seldom-0-x",link:"#seldom-0-x",children:[]},{level:3,title:"pyse",slug:"pyse",link:"#pyse",children:[]}],path:"/version/CHANGES.html",pathLocale:"/",extraFields:[]},{title:"浏览器与驱动",headers:[{level:3,title:"管理浏览器驱动",slug:"管理浏览器驱动",link:"#管理浏览器驱动",children:[]},{level:3,title:"指定浏览器驱动",slug:"指定浏览器驱动",link:"#指定浏览器驱动",children:[]},{level:3,title:"指定不同的浏览器",slug:"指定不同的浏览器",link:"#指定不同的浏览器",children:[]},{level:3,title:"控制浏览器启动和关闭",slug:"控制浏览器启动和关闭",link:"#控制浏览器启动和关闭",children:[]}],path:"/web-testing/browser_driver.html",pathLocale:"/",extraFields:[]},{title:"链式调用",headers:[{level:3,title:"基本例子",slug:"基本例子",link:"#基本例子",children:[]},{level:3,title:"Steps 类",slug:"steps-类",link:"#steps-类",children:[]},{level:3,title:"控制浏览器启动和关闭",slug:"控制浏览器启动和关闭",link:"#控制浏览器启动和关闭",children:[]}],path:"/web-testing/chaining.html",pathLocale:"/",extraFields:[]},{title:"浏览器启动配置",headers:[{level:3,title:"使用headless模式",slug:"使用headless模式",link:"#使用headless模式",children:[]},{level:3,title:"Selenium Grid",slug:"selenium-grid",link:"#selenium-grid",children:[]},{level:3,title:"Mobile Web 模式",slug:"mobile-web-模式",link:"#mobile-web-模式",children:[]},{level:3,title:"浏览器忽略无效证书",slug:"浏览器忽略无效证书",link:"#浏览器忽略无效证书",children:[]},{level:3,title:"浏览器关闭沙盒模式",slug:"浏览器关闭沙盒模式",link:"#浏览器关闭沙盒模式",children:[]},{level:3,title:"开启实验性功能",slug:"开启实验性功能",link:"#开启实验性功能",children:[]},{level:3,title:"设置浏览器代理",slug:"设置浏览器代理",link:"#设置浏览器代理",children:[]},{level:3,title:"连接已打开浏览器",slug:"连接已打开浏览器",link:"#连接已打开浏览器",children:[]}],path:"/web-testing/other.html",pathLocale:"/",extraFields:[]},{title:"Page Object",headers:[],path:"/web-testing/page_object.html",pathLocale:"/",extraFields:[]},{title:"Seldom API",headers:[{level:3,title:"查找元素",slug:"查找元素",link:"#查找元素",children:[]},{level:3,title:"断言",slug:"断言",link:"#断言",children:[]},{level:3,title:"WebDriver API",slug:"webdriver-api",link:"#webdriver-api",children:[]},{level:3,title:"键盘操作",slug:"键盘操作",link:"#键盘操作",children:[]},{level:3,title:"测试electron应用",slug:"测试electron应用",link:"#测试electron应用",children:[]}],path:"/web-testing/seldom_api.html",pathLocale:"/",extraFields:[]},{title:"",headers:[],path:"/404.html",pathLocale:"/",extraFields:[]}],km=me(wm),xm=()=>km,Lm=({searchIndex:e,routeLocale:t,query:n,maxSuggestions:r})=>{const o=F(()=>e.value.filter(s=>s.pathLocale===t.value));return F(()=>{const s=n.value.trim().toLowerCase();if(!s)return[];const l=[],i=(a,c)=>{ul(s,[c.title])&&l.push({link:`${a.path}#${c.slug}`,title:a.title,header:c.title});for(const u of c.children){if(l.length>=r.value)return;i(a,u)}};for(const a of o.value){if(l.length>=r.value)break;if(ul(s,[a.title,...a.extraFields])){l.push({link:a.path,title:a.title});continue}for(const c of a.headers){if(l.length>=r.value)break;i(a,c)}}return l})},Cm=e=>{const t=me(0);return{focusIndex:t,focusNext:()=>{t.value{t.value>0?t.value-=1:t.value=e.value.length-1}}},Tm=ue({name:"SearchBox",props:{locales:{type:Object,required:!1,default:()=>({})},hotKeys:{type:Array,required:!1,default:()=>[]},maxSuggestions:{type:Number,required:!1,default:5}},setup(e){const{locales:t,hotKeys:n,maxSuggestions:r}=Tr(e),o=Jt(),s=Kn(),l=xm(),i=me(null),a=me(!1),c=me(""),u=F(()=>t.value[s.value]??{}),f=Lm({searchIndex:l,routeLocale:s,query:c,maxSuggestions:r}),{focusIndex:h,focusNext:g,focusPrev:y}=Cm(f);Em({input:i,hotKeys:n});const w=F(()=>a.value&&!!f.value.length),C=()=>{w.value&&y()},v=()=>{w.value&&g()},b=P=>{if(!w.value)return;const S=f.value[P];S&&o.push(S.link).then(()=>{c.value="",h.value=0})};return()=>ae("form",{class:"search-box",role:"search"},[ae("input",{ref:i,type:"search",placeholder:u.value.placeholder,autocomplete:"off",spellcheck:!1,value:c.value,onFocus:()=>a.value=!0,onBlur:()=>a.value=!1,onInput:P=>c.value=P.target.value,onKeydown:P=>{switch(P.key){case"ArrowUp":{C();break}case"ArrowDown":{v();break}case"Enter":{P.preventDefault(),b(h.value);break}}}}),w.value&&ae("ul",{class:"suggestions",onMouseleave:()=>h.value=-1},f.value.map(({link:P,title:S,header:U},Z)=>ae("li",{class:["suggestion",{focus:h.value===Z}],onMouseenter:()=>h.value=Z,onMousedown:()=>b(Z)},ae("a",{href:P,onClick:$=>$.preventDefault()},[ae("span",{class:"page-title"},S),U&&ae("span",{class:"page-header"},`> ${U}`)]))))])}});const Sm={"/":{placeholder:"Search"},"/zh/":{placeholder:"搜索"}},Pm=["s","/"],Am=5,Om=Dt({enhance({app:e}){e.component("SearchBox",t=>ae(Tm,{locales:Sm,hotKeys:Pm,maxSuggestions:Am,...t}))}}),fl=()=>window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0,Rm=()=>window.scrollTo({top:0,behavior:"smooth"});const Im=ue({name:"BackToTop",setup(){const e=me(0),t=F(()=>e.value>300),n=Ei(()=>{e.value=fl()},100);Je(()=>{e.value=fl(),window.addEventListener("scroll",()=>n())});const r=ae("div",{class:"back-to-top",onClick:Rm});return()=>ae(qn,{name:"back-to-top"},()=>t.value?r:null)}}),$m=Dt({rootComponents:[Im]}),lr=[sd,cd,Ed,Ld,Ad,gm,Om,$m],Nm=[["v-fe360bd6","/develop.html",{title:""},["/develop","/develop.md"]],["v-d08d435a","/introduce.html",{title:"介绍"},["/introduce","/introduce.md"]],["v-8daa1a0e","/",{title:""},["/index.html","/README.md"]],["v-7cbb39b9","/api-testing/api_case.html",{title:"支持Excel测试用例"},["/api-testing/api_case","/api-testing/api_case.md"]],["v-109501ec","/api-testing/api_object.html",{title:"API Object"},["/api-testing/api_object","/api-testing/api_object.md"]],["v-d24a86f0","/api-testing/assert.html",{title:"更强大的断言"},["/api-testing/assert","/api-testing/assert.md"]],["v-5b62ef19","/api-testing/more.html",{title:"更多功能"},["/api-testing/more","/api-testing/more.md"]],["v-23f9483c","/api-testing/start.html",{title:"开始使用"},["/api-testing/start","/api-testing/start.md"]],["v-488b8cec","/api-testing/webscocket.html",{title:"WebSocket"},["/api-testing/webscocket","/api-testing/webscocket.md"]],["v-53c50d87","/app-testing/appium_lab.html",{title:"appium API"},["/app-testing/appium_lab","/app-testing/appium_lab.md"]],["v-483ce5fe","/app-testing/extensions.html",{title:"appium 扩展"},["/app-testing/extensions","/app-testing/extensions.md"]],["v-299549e4","/app-testing/page_object.html",{title:"Page Object"},["/app-testing/page_object","/app-testing/page_object.md"]],["v-4370387b","/app-testing/start.html",{title:"app 测试"},["/app-testing/start","/app-testing/start.md"]],["v-c350a662","/more-ability/db_operation.html",{title:"数据库操作"},["/more-ability/db_operation","/more-ability/db_operation.md"]],["v-1d715ea7","/more-ability/test_library.html",{title:"支持更多测试库"},["/more-ability/test_library","/more-ability/test_library.md"]],["v-d8f79a72","/getting-started/advanced.html",{title:"高级用法"},["/getting-started/advanced","/getting-started/advanced.md"]],["v-8e520eda","/getting-started/create_project.html",{title:"创建项目"},["/getting-started/create_project","/getting-started/create_project.md"]],["v-78c619cc","/getting-started/data_driver.html",{title:"数据驱动"},["/getting-started/data_driver","/getting-started/data_driver.md"]],["v-4e6f0425","/getting-started/dependent_func.html",{title:"方法的依赖"},["/getting-started/dependent_func","/getting-started/dependent_func.md"]],["v-4e8563af","/getting-started/installation.html",{title:"Installation"},["/getting-started/installation","/getting-started/installation.md"]],["v-0f898c79","/getting-started/quick_start.html",{title:"快速开始"},["/getting-started/quick_start","/getting-started/quick_start.md"]],["v-6f32df80","/getting-started/seldom_cli.html",{title:"seldom CLI"},["/getting-started/seldom_cli","/getting-started/seldom_cli.md"]],["v-3cdc5c3a","/platform/platform.html",{title:"平台化支持"},["/platform/platform","/platform/platform.md"]],["v-129a7066","/version/CHANGES.html",{title:"版本更新"},["/version/CHANGES","/version/CHANGES.md"]],["v-471218ee","/web-testing/browser_driver.html",{title:"浏览器与驱动"},["/web-testing/browser_driver","/web-testing/browser_driver.md"]],["v-198befa7","/web-testing/chaining.html",{title:"链式调用"},["/web-testing/chaining","/web-testing/chaining.md"]],["v-1f2c830c","/web-testing/other.html",{title:"浏览器启动配置"},["/web-testing/other","/web-testing/other.md"]],["v-0ccdb93b","/web-testing/page_object.html",{title:"Page Object"},["/web-testing/page_object","/web-testing/page_object.md"]],["v-40ed12b6","/web-testing/seldom_api.html",{title:"Seldom API"},["/web-testing/seldom_api","/web-testing/seldom_api.md"]],["v-3706649a","/404.html",{title:""},["/404"]]];var dl=ue({name:"Vuepress",setup(){const e=Wu();return()=>ae(e.value)}}),Dm=()=>Nm.reduce((e,[t,n,r,o])=>(e.push({name:t,path:n,component:dl,meta:r},...o.map(s=>({path:s,redirect:n}))),e),[{name:"404",path:"/:catchAll(.*)",component:dl}]),Mm=mf,Hm=()=>{const e=Zf({history:Mm(ui("/")),routes:Dm(),scrollBehavior:(t,n,r)=>r||(t.hash?{el:t.hash}:{top:0})});return e.beforeResolve(async(t,n)=>{var r;(t.path!==n.path||n===pt)&&([St.value]=await Promise.all([ht.resolvePageData(t.name),(r=hi[t.name])==null?void 0:r.__asyncLoader()]))}),e},Fm=e=>{e.component("ClientOnly",Ho),e.component("Content",Ju)},jm=(e,t,n)=>{const r=me(t.currentRoute.value.path);st(()=>t.currentRoute.value.path,h=>r.value=h);const o=F(()=>ht.resolveLayouts(n)),s=F(()=>ht.resolveRouteLocale(nn.value.locales,r.value)),l=F(()=>ht.resolveSiteLocaleData(nn.value,s.value)),i=F(()=>ht.resolvePageFrontmatter(St.value)),a=F(()=>ht.resolvePageHeadTitle(St.value,l.value)),c=F(()=>ht.resolvePageHead(a.value,i.value,l.value)),u=F(()=>ht.resolvePageLang(St.value)),f=F(()=>ht.resolvePageLayout(St.value,o.value));return e.provide(Bu,o),e.provide(mi,i),e.provide(Uu,a),e.provide(gi,c),e.provide(vi,u),e.provide(_i,f),e.provide(Do,s),e.provide(yi,l),Object.defineProperties(e.config.globalProperties,{$frontmatter:{get:()=>i.value},$head:{get:()=>c.value},$headTitle:{get:()=>a.value},$lang:{get:()=>u.value},$page:{get:()=>St.value},$routeLocale:{get:()=>s.value},$site:{get:()=>nn.value},$siteLocale:{get:()=>l.value},$withBase:{get:()=>Fo}}),{layouts:o,pageData:St,pageFrontmatter:i,pageHead:c,pageHeadTitle:a,pageLang:u,pageLayout:f,routeLocale:s,siteData:nn,siteLocaleData:l}},Bm=()=>{const e=Vu(),t=qu(),n=me([]),r=()=>{e.value.forEach(s=>{const l=zm(s);l&&n.value.push(l)})},o=()=>{document.documentElement.lang=t.value,n.value.forEach(s=>{s.parentNode===document.head&&document.head.removeChild(s)}),n.value.splice(0,n.value.length),e.value.forEach(s=>{const l=Vm(s);l!==null&&(document.head.appendChild(l),n.value.push(l))})};Wt(Ku,o),Je(()=>{r(),o(),st(()=>e.value,()=>o())})},zm=([e,t,n=""])=>{const r=Object.entries(t).map(([i,a])=>ge(a)?`[${i}=${JSON.stringify(a)}]`:a===!0?`[${i}]`:"").join(""),o=`head > ${e}${r}`;return Array.from(document.querySelectorAll(o)).find(i=>i.innerText===n)||null},Vm=([e,t,n])=>{if(!ge(e))return null;const r=document.createElement(e);return No(t)&&Object.entries(t).forEach(([o,s])=>{ge(s)?r.setAttribute(o,s):s===!0&&r.setAttribute(o,"")}),ge(n)&&r.appendChild(document.createTextNode(n)),r},Um=Iu,qm=async()=>{var n;const e=Um({name:"VuepressApp",setup(){var r;Bm();for(const o of lr)(r=o.setup)==null||r.call(o);return()=>[ae(Ri),...lr.flatMap(({rootComponents:o=[]})=>o.map(s=>ae(s)))]}}),t=Hm();Fm(e),jm(e,t,lr);for(const r of lr)await((n=r.enhance)==null?void 0:n.call(r,{app:e,router:t,siteData:nn}));return e.use(t),{app:e,router:t}};qm().then(({app:e,router:t})=>{t.isReady().then(()=>{e.mount("#app")})});export{Le as _,he as a,Nt as b,X as c,qm as createVueApp,te as d,Gc as e,j as o,bt as r}; diff --git a/assets/appium_lab.html-af1255b2.js b/assets/appium_lab.html-4a0939fc.js similarity index 80% rename from assets/appium_lab.html-af1255b2.js rename to assets/appium_lab.html-4a0939fc.js index bfac008..6193994 100644 --- a/assets/appium_lab.html-af1255b2.js +++ b/assets/appium_lab.html-4a0939fc.js @@ -1 +1 @@ -const i=JSON.parse('{"key":"v-53c50d87","path":"/app-testing/appium_lab.html","title":"appium API","lang":"en-US","frontmatter":{},"headers":[{"level":2,"title":"appium 定位","slug":"appium-定位","link":"#appium-定位","children":[]},{"level":2,"title":"appium lab","slug":"appium-lab","link":"#appium-lab","children":[]},{"level":2,"title":"appium driver","slug":"appium-driver","link":"#appium-driver","children":[]}],"git":{"updatedTime":1725890482000,"contributors":[{"name":"defnngj","email":"defnngj@gmail.com","commits":6},{"name":"fnngj","email":"fnngj@126.com","commits":1}]},"filePathRelative":"app-testing/appium_lab.md"}');export{i as data}; +const i=JSON.parse('{"key":"v-53c50d87","path":"/app-testing/appium_lab.html","title":"appium API","lang":"en-US","frontmatter":{},"headers":[{"level":2,"title":"appium 定位","slug":"appium-定位","link":"#appium-定位","children":[]},{"level":2,"title":"appium lab","slug":"appium-lab","link":"#appium-lab","children":[]},{"level":2,"title":"appium driver","slug":"appium-driver","link":"#appium-driver","children":[]}],"git":{"updatedTime":1734494711000,"contributors":[{"name":"defnngj","email":"defnngj@gmail.com","commits":7},{"name":"fnngj","email":"fnngj@126.com","commits":1}]},"filePathRelative":"app-testing/appium_lab.md"}');export{i as data}; diff --git a/assets/appium_lab.html-ab270943.js b/assets/appium_lab.html-ab270943.js deleted file mode 100644 index c17df79..0000000 --- a/assets/appium_lab.html-ab270943.js +++ /dev/null @@ -1,152 +0,0 @@ -import{_ as t,r as p,o as e,c as o,a as n,b as s,d as i,e as c}from"./app-72107ff1.js";const u={},l=c(`

    appium API

    appium API继承 selenium API,所以,操作方法是通用的。在seldom 中,请参考web UI 中的seldom API。

    appium 定位

    • 支持定位类型

    seldom 支持定位如下,包括selenium/appium。

    类型定位**kwargs
    selenium/appiumidid_="id"
    seleniummamename="name"
    selenium/appiumclassclass_name="class"
    seleniumtagtag="input"
    seleniumlink_textlink_text="文字链接"
    seleniumpartial_link_textpartial_link_text="文字链"
    selenium/appiumxpathxpath="//*[@id='11']"
    seleniumcsscass="input#id"
    appiumios_uiautomationios_uiautomation = "xx"
    appiumios_predicateios_predicate = "xx"
    appiumios_class_chainios_class_chain = "xx"
    appiumandroid_uiautomatorandroid_uiautomator = "xx"
    appiumandroid_viewtagandroid_viewtag = "xx"
    appiumandroid_data_matcherandroid_data_matcher = "xx"
    appiumandroid_view_matcherandroid_view_matcher = "xx"
    appiumwindows_uiautomationwindows_uiautomation = "xx"
    appiumaccessibility_idaccessibility_id = "xx"
    appiumimageimage = "xx"
    appiumcustomcustom = "xx"
    • 定位用法
    import seldom
    -from seldom.appium_lab.android import UiAutomator2Options
    -
    -
    -class TestBBS(seldom.TestCase):
    -
    -    def test_bbs(self):
    -        """定位方法用法"""
    -        self.click(id_="com.meizu.flyme.flymebbs:id/nw")
    -        self.sleep(2)
    -        self.type(android_uiautomator='new UiSelector().resourceId("com.meizu.flyme.flymebbs:id/nw")', text="flyme")
    -        ...
    -
    -
    -if __name__ == '__main__':
    -    capabilities = {
    -        "automationName": "UiAutomator2",
    -        "platformName": "Android",
    -        "appPackage": "com.meizu.flyme.flymebbs",
    -        "appActivity": "com.meizu.myplus.ui.splash.SplashActivity",
    -        "noReset": True,
    -    }
    -    options = UiAutomator2Options().load_capabilities(capabilities)
    -    seldom.main(app_server="http://127.0.0.1:4723", app_info=options, debug=True)
    -

    appium lab

    appium_lab 封装了常用App操作

    • 基本用法
    import seldom
    -from seldom.appium_lab import AppiumLab
    -
    -
    -class TestBBS(seldom.TestCase):
    -
    -    def start(self):
    -        # 导入 AppiumLab
    -        self.appium_lab = AppiumLab(self.driver)
    -
    -    def test_bbs(self):
    -        # 点击输入框
    -        self.click(id_="com.meizu.flyme.flymebbs:id/nw")
    -        self.sleep(2)
    -        # 判断当前虚拟键盘是否显示
    -        keyboard = self.appium_lab.is_keyboard_shown()
    -        print(keyboard)
    -        # 收起当前键盘
    -        self.appium_lab.hide_keyboard()
    -        self.sleep(3)
    -
    -
    -if __name__ == '__main__':
    -    ...
    -

    AppiumLab 类中分以下几类操作:

    Action

    Action中提供基本滑动/触摸操作。

    from seldom.appium_lab import AppiumLab
    -
    -appium_lab = AppiumLab()
    -# 触摸坐标位
    -appium_lab.tap(x=100, y=200)
    -# 上划
    -appium_lab.swipe_up()
    -# 下划
    -appium_lab.swipe_down()
    -# 左划
    -appium_lab.swipe_left()
    -# 右划
    -appium_lab.swipe_right()
    -

    Switch

    Switch中提供基本上下文切换操作。

    from seldom.appium_lab import AppiumLab
    -
    -appium_lab = AppiumLab()
    -
    -# 返回当前上下文
    -context = appium_lab.context()
    -# 切换原生app
    -appium_lab.switch_to_app()
    -# 切换webview
    -appium_lab.switch_to_web()
    -# 切换flutter
    -appium_lab.switch_to_flutter()
    -# 切换OCR
    -appium_lab.switch_to_ocr()
    -

    Find

    Find中提供基于文本的查找,一个元素可以没有ID、name,但一定有显示的文本,这里提供了一组基于文本的查找。

    from seldom.appium_lab import AppiumLab
    -
    -appium_lab = AppiumLab()
    -
    -# Android
    -appium_lab.find_view(text="xxx标题").click()
    -appium_lab.find_view(content_desc="xxx标题").click()
    -appium_lab.find_edit_text(text="xxx标题").click()
    -appium_lab.find_button(text="xxx标题").click()
    -appium_lab.find_button(content_desc="xxx标题").click()
    -appium_lab.find_text_view(text="xxx标题").click()
    -appium_lab.find_image_view(text="xxx标题").click()
    -appium_lab.find_check_box(text="xxx标题").click()
    -
    -# iOS
    -appium_lab.find_static_text(text="xxx标题").click()
    -appium_lab.find_other(text="xxx标题").click()
    -appium_lab.find_text_field(text="xxx标题").click()
    -appium_lab.find_image(text="xxx标题").click()
    -appium_lab.find_ios_button(text="xxx标题").click()
    -

    keyboard

    keyboard中提供基于键盘的输入和操作。

    from seldom.appium_lab import AppiumLab
    -
    -appium_lab = AppiumLab()
    -
    -# 基于键盘输入(支持大小写)
    -appium_lab.key_text("Hello123")
    -# 手机home键
    -appium_lab.home()
    -# 手机返回键
    -appium_lab.back()
    -# 判断当前虚拟键盘是否显示(True/False)
    -ret = appium_lab.is_keyboard_shown()
    -print(ret)
    -# 收起虚拟键盘
    -appium_lab.hide_keyboard()
    -# 返回当前窗口尺寸
    -size = appium_lab.size()
    -

    appium driver

    AppDriver 封装了App相关的操作。

    import seldom
    -
    -
    -class TestApp(seldom.TestCase):
    -    """
    -    Test App
    -    """
    -
    -    def test_bbs_search(self):
    -        """
    -        appium api
    -        """
    -        # app置于后台10s
    -        self.background_app(10)
    -        # 检查设备上是否安装了应用程序
    -        self.is_app_installed("bundle_id")
    -        # 安装app
    -        self.install_app("/app/path/xxx.apk")
    -        # 删除app
    -        self.remove_app("app_id")
    -        # 启动app
    -        self.launch_app()
    -        # 关闭app
    -        self.close_app()
    -        # 如果app正在运行,终止运行
    -        self.terminate_app("app_id")
    -        # 如果app未运行,则激活它或者在后台运行
    -        self.activate_app("app_id")
    -        # 查询app 状态
    -        state = self.query_app_state("app_id")
    -        print(state)
    -        # 从指定的设备返回应用程序字符串语言
    -        language, string = self.app_strings()
    -        print(language, string)
    -        # 启动起app
    -        self.reset()
    -        # 点击图片
    -        self.click_image("/you/path/xxx.png")
    -
    -
    `,28),d={href:"https://github.com/SeldomQA/seldom/issues",target:"_blank",rel:"noopener noreferrer"};function r(k,m){const a=p("ExternalLinkIcon");return e(),o("div",null,[l,n("blockquote",null,[n("p",null,[s("目前 seldom 集成的 appium API 并不完整,在使用过程中如有问题,欢迎提 "),n("a",d,[s("issues"),i(a)]),s("。")])])])}const b=t(u,[["render",r],["__file","appium_lab.html.vue"]]);export{b as default}; diff --git a/assets/appium_lab.html-8deae398.js b/assets/appium_lab.html-d907fadd.js similarity index 84% rename from assets/appium_lab.html-8deae398.js rename to assets/appium_lab.html-d907fadd.js index 63e813b..3f4b921 100644 --- a/assets/appium_lab.html-8deae398.js +++ b/assets/appium_lab.html-d907fadd.js @@ -1,4 +1,4 @@ -import{_ as t,r as p,o as e,c as o,a as n,b as s,d as i,e as c}from"./app-a06a2d51.js";const u={},l=c(`

    appium API

    appium API继承 selenium API,所以,操作方法是通用的。在seldom 中,请参考web UI 中的seldom API。

    appium 定位

    • 支持定位类型

    seldom 支持定位如下,包括selenium/appium。

    类型定位**kwargs
    selenium/appiumidid_="id"
    seleniummamename="name"
    selenium/appiumclassclass_name="class"
    seleniumtagtag="input"
    seleniumlink_textlink_text="文字链接"
    seleniumpartial_link_textpartial_link_text="文字链"
    selenium/appiumxpathxpath="//*[@id='11']"
    seleniumcsscass="input#id"
    appiumios_uiautomationios_uiautomation = "xx"
    appiumios_predicateios_predicate = "xx"
    appiumios_class_chainios_class_chain = "xx"
    appiumandroid_uiautomatorandroid_uiautomator = "xx"
    appiumandroid_viewtagandroid_viewtag = "xx"
    appiumandroid_data_matcherandroid_data_matcher = "xx"
    appiumandroid_view_matcherandroid_view_matcher = "xx"
    appiumwindows_uiautomationwindows_uiautomation = "xx"
    appiumaccessibility_idaccessibility_id = "xx"
    appiumimageimage = "xx"
    appiumcustomcustom = "xx"
    • 定位用法
    import seldom
    +import{_ as t,r as p,o as e,c as o,a as n,b as s,d as i,e as c}from"./app-9fb6f1b5.js";const u={},l=c(`

    appium API

    appium API继承 selenium API,所以,操作方法是通用的。在seldom 中,请参考web UI 中的seldom API。

    appium 定位

    • 支持定位类型

    seldom 支持定位如下,包括selenium/appium。

    类型定位**kwargs
    selenium/appiumidid_="id"
    seleniummamename="name"
    selenium/appiumclassclass_name="class"
    seleniumtagtag="input"
    seleniumlink_textlink_text="文字链接"
    seleniumpartial_link_textpartial_link_text="文字链"
    selenium/appiumxpathxpath="//*[@id='11']"
    seleniumcsscass="input#id"
    appiumios_uiautomationios_uiautomation = "xx"
    appiumios_predicateios_predicate = "xx"
    appiumios_class_chainios_class_chain = "xx"
    appiumandroid_uiautomatorandroid_uiautomator = "xx"
    appiumandroid_viewtagandroid_viewtag = "xx"
    appiumandroid_data_matcherandroid_data_matcher = "xx"
    appiumandroid_view_matcherandroid_view_matcher = "xx"
    appiumwindows_uiautomationwindows_uiautomation = "xx"
    appiumaccessibility_idaccessibility_id = "xx"
    appiumimageimage = "xx"
    appiumcustomcustom = "xx"
    • 定位用法
    import seldom
     from seldom.appium_lab.android import UiAutomator2Options
     
     
    @@ -46,7 +46,18 @@ import{_ as t,r as p,o as e,c as o,a as n,b as s,d as i,e as c}from"./app-a06a2d
     
     if __name__ == '__main__':
         ...
    -

    AppiumLab 类中分以下几类操作:

    Action

    Action中提供基本滑动/触摸操作。

    from seldom.appium_lab import AppiumLab
    +
    • 启动appium server
    from seldom.appium_lab.appium_service import AppiumService
    +
    +if __name__ == '__main__':
    +    # 启动 Appium Server
    +    app_ser = AppiumService(
    +        addr="127.0.0.1",
    +        port="4723",
    +        use_plugins="images",
    +        args=["--allow-cors", "--tmp", "C:\\Windows\\Temp"])
    +    app_ser.start_service()
    +

    参数说明:

    • addr: appium server 地址, 默认: 127.0.0.1
    • port: appium server 端口, 默认:4723
    • log: 设置 appium server 日志, 默认:appium_server_1734493548.log
    • use_plugins: 设置使用的插件,默认None,不使用。
    • args: 支持添加更多的参数,例如 args=["--allow-cors", "--tmp", "C:\\Windows\\Temp"]

    启动日志:

    2024-12-18 11:52:54 | INFO     | appium_service.py | MainThread | 🚀 launch appium server: ['--address', '127.0.0.1', '--port', '4723', '--log', 'D:\\\\github\\\\seldomQA\\\\seldom\\\\seldom\\\\appium_lab\\\\appium_server_1734493974.log', '--use-plugins', 'iamges,ocr', '--allow-cors']
    +

    AppiumLab 类中分以下几类操作:

    Action

    Action中提供基本滑动/触摸操作。

    from seldom.appium_lab import AppiumLab
     
     appium_lab = AppiumLab()
     # 触摸坐标位
    @@ -59,7 +70,9 @@ appium_lab.swipe_down.swipe_left()
     # 右划
     appium_lab.swipe_right()
    -

    Switch

    Switch中提供基本上下文切换操作。

    from seldom.appium_lab import AppiumLab
    +# 从x坐标滑动到y坐标
    +appium_lab.drag_from_to()
    +

    Switch

    Switch中提供基本上下文切换操作。

    from seldom.appium_lab import AppiumLab
     
     appium_lab = AppiumLab()
     
    @@ -130,10 +143,6 @@ size = appium_lab.install_app("/app/path/xxx.apk")
             # 删除app
             self.remove_app("app_id")
    -        # 启动app
    -        self.launch_app()
    -        # 关闭app
    -        self.close_app()
             # 如果app正在运行,终止运行
             self.terminate_app("app_id")
             # 如果app未运行,则激活它或者在后台运行
    @@ -144,9 +153,7 @@ size = appium_lab# 从指定的设备返回应用程序字符串语言
             language, string = self.app_strings()
             print(language, string)
    -        # 启动起app
    -        self.reset()
             # 点击图片
             self.click_image("/you/path/xxx.png")
     
    -
    `,28),d={href:"https://github.com/SeldomQA/seldom/issues",target:"_blank",rel:"noopener noreferrer"};function r(k,m){const a=p("ExternalLinkIcon");return e(),o("div",null,[l,n("blockquote",null,[n("p",null,[s("目前 seldom 集成的 appium API 并不完整,在使用过程中如有问题,欢迎提 "),n("a",d,[s("issues"),i(a)]),s("。")])])])}const b=t(u,[["render",r],["__file","appium_lab.html.vue"]]);export{b as default}; +
    `,34),d={href:"https://github.com/SeldomQA/seldom/issues",target:"_blank",rel:"noopener noreferrer"};function r(k,m){const a=p("ExternalLinkIcon");return e(),o("div",null,[l,n("blockquote",null,[n("p",null,[s("目前 seldom 集成的 appium API 并不完整,在使用过程中如有问题,欢迎提 "),n("a",d,[s("issues"),i(a)]),s("。")])])])}const b=t(u,[["render",r],["__file","appium_lab.html.vue"]]);export{b as default}; diff --git a/assets/assert.html-82cebf94.js b/assets/assert.html-82cebf94.js deleted file mode 100644 index 42daf7c..0000000 --- a/assets/assert.html-82cebf94.js +++ /dev/null @@ -1,98 +0,0 @@ -import{_ as n,o as s,c as a,e as t}from"./app-a06a2d51.js";const p={},o=t(`

    更强大的断言

    断言接口返回的数据是HTTP接口自动化测试非常重要的工作,提供强大的断言方法可以提高用例的编写效率。

    assertJSON

    assertJSON() 断言接口返回的某部分数据。

    • 请求参数
    {
    -  "name": "tom",
    -  "hobby": [
    -    "basketball",
    -    "swim"
    -  ]
    -}
    -
    • 返回结果
    {
    -  "args": {
    -    "hobby": [
    -      "basketball",
    -      "swim"
    -    ],
    -    "name": "tom"
    -  },
    -  "headers": {
    -    "Accept": "*/*",
    -    "Accept-Encoding": "gzip, deflate",
    -    "Host": "httpbin.org",
    -    "User-Agent": "python-requests/2.25.0",
    -    "X-Amzn-Trace-Id": "Root=1-62851614-1ca9fdb276238c60406c118f"
    -  },
    -  "origin": "113.87.15.99",
    -  "url": "http://httpbin.org/get?name=tom&hobby=basketball&hobby=swim"
    -}
    -

    我的目标是断言namehobby 部分的内容。

    import seldom
    -
    -
    -class TestAPI(seldom.TestCase):
    -
    -    def test_assert_json(self):
    -        # 接口参数
    -        payload = {"name": "tom", "hobby": ["basketball", "swim"]}
    -        # 接口调用
    -        self.get("http://httpbin.org/get", params=payload)
    -
    -        # 断言数据
    -        assert_data = {
    -            "hobby": ["swim", "basketball"],
    -            "name": "tom"
    -        }
    -        self.assertJSON(assert_data, self.response["args"], exclude=["xxx"])
    -
    • exclude 用于设置跳过的检查字段,例如一些 时间、随机数 等,每次调用都不一样,但并不影响结果的正确性。通过 exclude 来设置屏蔽这些字段的检查。

    assertPath

    assertPath 是基于 jmespath 实现的断言,功能非常强大。

    • jmespath: https://jmespath.org/specification.html

    接口返回数据如下:

    {
    -  "args": {
    -    "hobby": 
    -      ["basketball", "swim"], 
    -    "name": "tom"
    -  }
    -}
    -

    seldom中可以通过path进行断言:

    import seldom
    -
    -
    -class TestAPI(seldom.TestCase):
    -
    -    def test_assert_path(self):
    -        payload = {'name': 'tom', 'hobby': ['basketball', 'swim']}
    -        self.get("http://httpbin.org/get", params=payload)
    -        self.assertPath("args.name", "tom")
    -        self.assertPath("args.hobby[0]", "basketball")
    -        self.assertInPath("args.hobby[0]", "ball")
    -
    -
    • args.hobby[0] 提取接口返回的数据。
    • assertPath() 判断提取的数据是否等于basketball;
    • assertInPath() 判断提取的数据是否包含ball

    assertSchema

    当你不关心数据本身是什么,而是关心数据的结构和类型时,可以使用 assertSchema 断言方法。 assertSchema 是基于 jsonschema 实现的断言方法。

    • jsonschema: https://json-schema.org/learn/
    import seldom
    -from seldom.utils import genson
    -
    -
    -class TestAPI(seldom.TestCase):
    -
    -    def test_assert_schema(self):
    -        # 接口参数
    -        payload = {"hobby": ["basketball", "swim"], "name": "tom", "age": "18"}
    -        # 调用接口
    -        self.get("/get", params=payload)
    -
    -        # 生成数据结构和类型
    -        schema = genson(self.response["args"])
    -        print("json Schema: \\n", schema)
    -
    -        # 断言数据结构和类型
    -        assert_data = {
    -            "$schema": "http://json-schema.org/schema#",
    -            "type": "object",
    -            "properties": {
    -                "age": {
    -                    "type": "string"
    -                },
    -                "hobby": {
    -                    "type": "array", "items": {"type": "string"}
    -                },
    -                "name": {
    -                    "type": "string"
    -                }
    -            },
    -            "required":
    -                ["age", "hobby", "name"]
    -        }
    -        self.assertSchema(assert_data, self.response["args"])
    -
    -
    • genson: 可以生成jsonschema数据结构和类型(seldom 2.9 新增)。
    `,24),e=[o];function c(u,i){return s(),a("div",null,e)}const r=n(p,[["render",c],["__file","assert.html.vue"]]);export{r as default}; diff --git a/assets/assert.html-54753e7a.js b/assets/assert.html-d4c63b80.js similarity index 99% rename from assets/assert.html-54753e7a.js rename to assets/assert.html-d4c63b80.js index 55eb3ca..e088833 100644 --- a/assets/assert.html-54753e7a.js +++ b/assets/assert.html-d4c63b80.js @@ -1,4 +1,4 @@ -import{_ as n,o as s,c as a,e as t}from"./app-72107ff1.js";const p={},o=t(`

    更强大的断言

    断言接口返回的数据是HTTP接口自动化测试非常重要的工作,提供强大的断言方法可以提高用例的编写效率。

    assertJSON

    assertJSON() 断言接口返回的某部分数据。

    • 请求参数
    {
    +import{_ as n,o as s,c as a,e as t}from"./app-9fb6f1b5.js";const p={},o=t(`

    更强大的断言

    断言接口返回的数据是HTTP接口自动化测试非常重要的工作,提供强大的断言方法可以提高用例的编写效率。

    assertJSON

    assertJSON() 断言接口返回的某部分数据。

    • 请求参数
    {
       "name": "tom",
       "hobby": [
         "basketball",
    diff --git a/assets/browser_driver.html-299e0e9f.js b/assets/browser_driver.html-7d1718c1.js
    similarity index 99%
    rename from assets/browser_driver.html-299e0e9f.js
    rename to assets/browser_driver.html-7d1718c1.js
    index 87c4600..3e6fa97 100644
    --- a/assets/browser_driver.html-299e0e9f.js
    +++ b/assets/browser_driver.html-7d1718c1.js
    @@ -1,4 +1,4 @@
    -import{_ as n,o as s,c as a,e}from"./app-72107ff1.js";const t={},p=e(`

    浏览器与驱动

    管理浏览器驱动

    seldom 2.3.0 版本集成webdriver_manager管理浏览器驱动。

    seldom 3.3.0 版本移除了webdriver_manager,selenium 4.6 之后内置了 selenium-manager 可以自动管理浏览器驱动。

    自动下载

    如果你不配置浏览器驱动也没关系,seldom(selenium)会根据你使用的浏览器版本,自动化下载对应的驱动文件。

    • 编写简单的用例
    import seldom
    +import{_ as n,o as s,c as a,e}from"./app-9fb6f1b5.js";const t={},p=e(`

    浏览器与驱动

    管理浏览器驱动

    seldom 2.3.0 版本集成webdriver_manager管理浏览器驱动。

    seldom 3.3.0 版本移除了webdriver_manager,selenium 4.6 之后内置了 selenium-manager 可以自动管理浏览器驱动。

    自动下载

    如果你不配置浏览器驱动也没关系,seldom(selenium)会根据你使用的浏览器版本,自动化下载对应的驱动文件。

    • 编写简单的用例
    import seldom
     
     
     class BingTest(seldom.TestCase):
    diff --git a/assets/browser_driver.html-d273cebd.js b/assets/browser_driver.html-d273cebd.js
    deleted file mode 100644
    index 994f03e..0000000
    --- a/assets/browser_driver.html-d273cebd.js
    +++ /dev/null
    @@ -1,118 +0,0 @@
    -import{_ as n,o as s,c as a,e}from"./app-a06a2d51.js";const t={},p=e(`

    浏览器与驱动

    管理浏览器驱动

    seldom 2.3.0 版本集成webdriver_manager管理浏览器驱动。

    seldom 3.3.0 版本移除了webdriver_manager,selenium 4.6 之后内置了 selenium-manager 可以自动管理浏览器驱动。

    自动下载

    如果你不配置浏览器驱动也没关系,seldom(selenium)会根据你使用的浏览器版本,自动化下载对应的驱动文件。

    • 编写简单的用例
    import seldom
    -
    -
    -class BingTest(seldom.TestCase):
    -
    -    def test_bing_search(self):
    -        """selenium api"""
    -        self.open("http://www.bing.com")
    -        self.type(id_="sb_form_q", text="seldom", enter=True)
    -        self.sleep(2)
    -        self.assertTitle("seldom - 搜索")
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(browser="edge", debug=True)
    -

    selenium驱动检查逻辑:

    1. 首先判断 环境变量 PATH 是否配置了浏览器驱动。 通过where 查找命令位置,如果可以找到说明,已配置了,环境变量PATH
    > where msedgedriver
    -D:\\webdriver\\msedgedriver.exe
    -
    1. 如果没有找到浏览器驱动,会根据当前浏览器版本,查找对应驱动文件下载。 selenium-manager 可以查看浏览器驱动的默认安装路径。
    > selenium-manager --driver msedgedriver
    -INFO    Driver path: C:\\Users\\xxx\\.cache\\selenium\\msedgedriver\\win64\\116.0.1938.76\\msedgedriver.exe
    -INFO    Browser path: C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe
    -

    手动下载

    通过 selenium-manager 命令下载浏览器驱动,需要知道每个浏览器驱动的名字。

    > selenium-manager --driver chromedriver  # chrome
    -> selenium-manager --driver msedgedriver  # edge
    -> selenium-manager --driver geckodriver   # firefox
    -

    指定浏览器驱动

    虽然,selenium-manager可以方便的管理浏览器驱动,但selenium-manager自动下载浏览器驱动很慢,有些环境也不是方便。

    seldom 3.7 版本重新支持 executable_path 参数,指定浏览器驱动。

    import seldom
    -
    -# ……
    -
    -if __name__ == '__main__':
    -    browser = {
    -        "browser": "chrome",
    -        "executable_path": "D:\\webdriver\\chromedriver.exe",  # 设置chrome浏览器驱动位置,其他浏览器类似。
    -    }
    -    seldom.main(browser=browser)
    -

    指定不同的浏览器

    我们运行的自动化测试不可能只在一个浏览器下运行,我们分别需要在chrome、firefox浏览器下运行。在seldom中需要只需要修改一个配置即可。

    import seldom
    -
    -# ……
    -
    -if __name__ == '__main__':
    -    seldom.main(browser="chrome")  # chrome浏览器,默认值
    -    seldom.main(browser="gc")  # google chrome简写
    -    seldom.main(browser="firefox")  # firefox浏览器
    -    seldom.main(browser="ff")  # firefox简写
    -    seldom.main(browser="edge")  # edge浏览器
    -    seldom.main(browser="safari")  # safari浏览器
    -    seldom.main(browser="ie")  # internet explore浏览器
    -

    main()方法中通过browser参数设置不同的浏览器,默认为Chrome浏览器。

    控制浏览器启动和关闭

    seldom 默认通过seldom.main(browser="edge")全局设置浏览器的启动和关闭,一般我们不需要关心浏览器的启动和关闭。

    seldom 3.9.0 支持手动控制浏览器的驱动和关闭。

    • 每个用例启动和关闭浏览器。
    import seldom
    -
    -
    -class WebTestOne(seldom.TestCase):
    -    """case lunch browser"""
    -
    -    def start(self):
    -        self.browser("edge")
    -
    -    def end(self):
    -        self.quit()
    -
    -    def test_baidu(self):
    -        self.open("http://www.baidu.com")
    -        ...
    -
    -    def test_bing(self):
    -        self.open("http://cn.bing.com")
    -        ...
    -
    -
    -if __name__ == '__main__':
    -    seldom.main()
    -
    • 每个类启动和关闭浏览器。
    import seldom
    -
    -
    -class WebTestTwo(seldom.TestCase):
    -    """class lunch browser"""
    -
    -    @classmethod
    -    def start_class(cls):
    -        cls.browser(cls, "gc")
    -
    -    @classmethod
    -    def end_class(cls):
    -        cls.quit(cls)
    -
    -    def test_baidu(self):
    -        self.open("http://www.baidu.com")
    -        ...
    -
    -    def test_bing(self):
    -        self.open("http://cn.bing.com")
    -        ...
    -
    -
    -if __name__ == '__main__':
    -    seldom.main()
    -
    • 打开一个新的浏览器

    seldom 默认会启动一个浏览器,在运行的过程中需要打开一个新的浏览器执行其他操作,可以使用new_browser()方法。

    import seldom
    -
    -
    -class WebTestNew(seldom.TestCase):
    -    """Web search test case"""
    -
    -    def test_new_browser(self):
    -        # default browser
    -        self.open("http://www.baidu.com")
    -        self.Keys(css="#kw").input("seldom").enter()
    -        self.sleep(2)
    -        self.screenshots()
    -        self.assertInTitle("seldom")
    -
    -        # open new browser
    -        browser = self.new_browser()
    -        browser.open("http://cn.bing.com")
    -        browser.type(id_="sb_form_q", text="seldom", enter=True)
    -        self.sleep(2)
    -        browser.screenshots()
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(browser="edge")
    -
    `,33),o=[p];function c(i,l){return s(),a("div",null,o)}const r=n(t,[["render",c],["__file","browser_driver.html.vue"]]);export{r as default}; diff --git a/assets/chaining.html-47993268.js b/assets/chaining.html-4d285868.js similarity index 99% rename from assets/chaining.html-47993268.js rename to assets/chaining.html-4d285868.js index 702eaee..61c4480 100644 --- a/assets/chaining.html-47993268.js +++ b/assets/chaining.html-4d285868.js @@ -1,4 +1,4 @@ -import{_ as n,o as s,c as a,e as t}from"./app-a06a2d51.js";const p={},o=t(`

    链式调用

    方法链接是一种技术,用于对同一个对象进行多个方法调用,只使用一次对象引用。

    基本例子

    先来看一下如何通过seldom使用链式调用编写Web测试用例。

    import seldom
    +import{_ as n,o as s,c as a,e as t}from"./app-9fb6f1b5.js";const p={},o=t(`

    链式调用

    方法链接是一种技术,用于对同一个对象进行多个方法调用,只使用一次对象引用。

    基本例子

    先来看一下如何通过seldom使用链式调用编写Web测试用例。

    import seldom
     from seldom import Steps
     
     
    diff --git a/assets/chaining.html-5cd990f6.js b/assets/chaining.html-5cd990f6.js
    deleted file mode 100644
    index 3f9fd85..0000000
    --- a/assets/chaining.html-5cd990f6.js
    +++ /dev/null
    @@ -1,119 +0,0 @@
    -import{_ as n,o as s,c as a,e as t}from"./app-72107ff1.js";const p={},o=t(`

    链式调用

    方法链接是一种技术,用于对同一个对象进行多个方法调用,只使用一次对象引用。

    基本例子

    先来看一下如何通过seldom使用链式调用编写Web测试用例。

    import seldom
    -from seldom import Steps
    -
    -
    -class BaiduTest(seldom.TestCase):
    -
    -    def test_search_one(self):
    -        """
    -        百度搜索
    -        """
    -        Steps(desc="百度搜索").open("http://www.baidu.com").find("#kw").type("seldom").find("#su").click()
    -        self.assertInTitle("seldom")
    -
    -    def test_search_two(self):
    -        """
    -        百度搜索
    -        """
    -        s = Steps(desc="百度搜索")
    -        s.open("http://www.baidu.com")
    -        s.find("#kw").type("seldom").enter()
    -        self.assertInTitle("seldom")
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(browser="gc", tester="虫师")
    -

    用例像链条一样将整个测试过程串联起来,当然,如果你讨厌换行符\\,也可以将用例分成多次调用,总之,只要你愿意,可以将所有步骤都串联起来。

    import seldom
    -from seldom import Steps
    -
    -
    -class BaiduTest(seldom.TestCase):
    -
    -    def test_search_setting(self):
    -        """百度搜索设置"""
    -        Steps(url="http://www.baidu.com", desc="百度搜索设置")
    -            .open()
    -            .find("#s-usersetting-top").click()
    -            .find("#s-user-setting-menu > div > a.setpref").click().sleep(2)
    -            .find('[data-tabid="advanced"]').click().sleep(2)
    -            .find("#q5_1").click().sleep(2)
    -            .find('[data-tabid="general"]').click().sleep(2)
    -            .find_text("保存设置").click()
    -            .alert().accept()
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(browser="gc", tester="虫师")
    -

    Steps 类

    Steps 类所提供的API 大部分和Webidrver 类保持一致,但考虑掉到链式的特点,命名上更体现动作

    查找元素

    from seldom import Steps
    -
    -c = Steps()
    -c.find("#id")
    -c.find(".class")
    -c.find("[name=password]")
    -c.find("div > tr > td")
    -c.find("div", 1)
    -c.find("text=hao123")
    -c.find("text*=hao1")
    -
    -c.find_text("新闻")
    -
    • find(): 只支持CSS定位,这几乎是最强大的定位方法了。 新的测试库cypressplaywright 默认也都是CSS定位。

      • text= 用来定位文本,相当于find_text()
      • test*= 用例模糊定位文本。
    • find_text(): 用于定位文本。

    操作方法

    import seldom
    -from seldom import Steps
    -
    -
    -class TestCase(seldom.TestCase):
    -
    -    def test_chaining_api(self):
    -        Steps(desc="chaining api")
    -            .open("https://www.baidu.com")
    -            .max_window()
    -            .set_window(800, 600)
    -            .find("css").clear()
    -            .find("css").type("seldom")
    -            .find("css").enter()
    -            .find("css").submit()
    -            .find("css").click()
    -            .find("css").double_click()
    -            .find("css").move_to_click()
    -            .find("css").click_and_hold()
    -            .find("css").switch_to_frame()
    -            .find("css").select(value="")
    -            .find("css").select(text="每页显示20条")
    -            .find("css").select(index=2)
    -            .switch_to_frame_out()
    -            .switch_to_window(1)
    -            .refresh()
    -            .alert().accept()
    -            .alert().dismiss()
    -            .screenshots()
    -            .element_screenshot()
    -            .sleep(1)
    -            .close()
    -            .quit()
    -
    -
    1. 基于元素定位的操作先调用find()/find_text(), 例如type(), click() 等。

    2. accept()/dismiss() 是基于alert的操作。

    控制浏览器启动和关闭

    seldom 默认通过seldom.main(browser="edge")全局设置浏览器的启动和关闭,一般我们不需要关心浏览器的启动和关闭。

    seldom 3.9.0 支持手动控制浏览器的驱动和关闭。

    • 每个用例启动和关闭浏览器。
    import seldom
    -from seldom import Steps
    -
    -
    -class WebTestChaining(seldom.TestCase):
    -    """test chaining API"""
    -
    -    def start(self):
    -        self.step = Steps(browser="edge")
    -
    -    def end(self):
    -        self.step.quit()
    -
    -    def test_baidu(self):
    -        """test baidu search"""
    -        self.step.open("https://www.baidu.com").find("#kw").type("seldom").find("#su").click().sleep(2)
    -        self.assertInTitle("seldom")
    -
    -    def test_bing(self):
    -        """test bing search"""
    -        self.step.open("https://www.bing.com").find("#sb_form_q").type("seldomqa").submit().sleep(2)
    -        self.assertInTitle("seldomqa")
    -
    -
    -if __name__ == '__main__':
    -    seldom.main()
    -
    `,20),e=[o];function c(u,i){return s(),a("div",null,e)}const k=n(p,[["render",c],["__file","chaining.html.vue"]]);export{k as default}; diff --git a/assets/create_project.html-385e2a55.js b/assets/create_project.html-1b343fd7.js similarity index 99% rename from assets/create_project.html-385e2a55.js rename to assets/create_project.html-1b343fd7.js index ff6e9ae..16205c9 100644 --- a/assets/create_project.html-385e2a55.js +++ b/assets/create_project.html-1b343fd7.js @@ -1,4 +1,4 @@ -import{_ as s,o as a,c as n,e}from"./app-72107ff1.js";const l={},i=e(`

    创建项目

    seldom已经安装完成,那么现在已经迫不及待的想体验seldom的使用。

    自动生成项目

    seldom 通过seldom命令提供了脚手架,可以快速的帮我们创建自动化测试项目。

    1. 查看帮助:
    > seldom --help
    +import{_ as s,o as a,c as n,e}from"./app-9fb6f1b5.js";const l={},i=e(`

    创建项目

    seldom已经安装完成,那么现在已经迫不及待的想体验seldom的使用。

    自动生成项目

    seldom 通过seldom命令提供了脚手架,可以快速的帮我们创建自动化测试项目。

    1. 查看帮助:
    > seldom --help
     Usage: seldom [OPTIONS]
     
       seldom CLI.
    diff --git a/assets/create_project.html-9dc05755.js b/assets/create_project.html-9dc05755.js
    deleted file mode 100644
    index 10ad273..0000000
    --- a/assets/create_project.html-9dc05755.js
    +++ /dev/null
    @@ -1,63 +0,0 @@
    -import{_ as s,o as a,c as n,e}from"./app-a06a2d51.js";const l={},i=e(`

    创建项目

    seldom已经安装完成,那么现在已经迫不及待的想体验seldom的使用。

    自动生成项目

    seldom 通过seldom命令提供了脚手架,可以快速的帮我们创建自动化测试项目。

    1. 查看帮助:
    > seldom --help
    -Usage: seldom [OPTIONS]
    -
    -  seldom CLI.
    -
    -Options:
    -  --version                       Show version.
    -  --project-api TEXT              Create an API automation test project.
    -  --project-app TEXT              Create an App automation test project.
    -  --project-web TEXT              Create an Web automation test project.
    -  -cc, --clear-cache BOOLEAN      Clear all caches of seldom.
    -  -p, --path TEXT                 Run test case file path.
    -  -c, --collect / -nc, --no-collect
    -                                  Collect project test cases. Need the
    -                                  \`--path\`.
    -  -l, --level [data|method]       Parse the level of use cases. Need the
    -                                  --path.
    -  -j, --case-json TEXT            Test case files. Need the \`--path\`.
    -  -e, --env TEXT                  Set the Seldom run environment \`Seldom.env\`.
    -  -b, --browser [chrome|firefox|ie|edge|safari]
    -                                  The browser that runs the Web UI automation
    -                                  tests. Need the \`--path\`.
    -  -u, --base-url TEXT             The base-url that runs the HTTP automation
    -                                  tests. Need the \`--path\`.
    -  -d, --debug / -nd, --no-debug   Debug mode. Need the \`--path\`.
    -  -rr, --rerun INTEGER            The number of times a use case failed to run
    -                                  again. Need the \`--path\`.
    -  -r, --report TEXT               Set the test report for output. Need the
    -                                  \`--path\`.
    -  -m, --mod TEXT                  Run tests modules, classes or even
    -                                  individual test methods from the command
    -                                  line.
    -  -ll, --log-level [TRACE|DEBUG|INFO|SUCCESS|WARNING|ERROR]
    -                                  Set the log level.
    -  -h2c, --har2case TEXT           HAR file converts an seldom test case.
    -  -s2c, --swagger2case TEXT       Swagger file converts an seldom test case.
    -  --api-excel TEXT                Run the api test cases in the excel file.
    -  --help                          Show this message and exit.
    -
    1. 创建项目:
    > seldom -P mypro
    -

    目录结构如下:

    mypro/
    -├── test_dir/
    -│   ├── __init__.py
    -│   ├── test_web_sample.py
    -│   ├── test_api_sample.py
    -├── test_data/
    -│   ├── data.json
    -├── reports/
    -└── confrun.py
    -
    • test_dir/ 测试用例目录。
    • test_data/ 测试数据文件目录。
    • reports/ 测试报告目录。
    • confrun.py 运行测试用例配置文件。
    1. 克隆项目

    如果无法使用seldom命令,可以通过git克隆相关项目进行学习。

    • seldom-web-testing
    > git clone https://github.com/SeldomQA/seldom-web-testing
    -
    • seldom-api-testing
    > git clone https://github.com/defnngj/seldom-api-testing
    -

    创建测试用例

    根据上面的创建的项目,可以在test_dir目录下继续创建测试用例:test_sample.py

    import seldom
    -
    -
    -class YouTest(seldom.TestCase):
    -
    -    def test_case(self):
    -        """a simple test case """
    -        ...
    -
    -
    -if __name__ == '__main__':
    -    seldom.main()
    -

    根据自己的需求编写Web UIApp UIHTTP接口自动化测试。

    `,21),t=[i];function p(c,o){return a(),n("div",null,t)}const d=s(l,[["render",p],["__file","create_project.html.vue"]]);export{d as default}; diff --git a/assets/data_driver.html-b253800d.js b/assets/data_driver.html-52b7c40f.js similarity index 99% rename from assets/data_driver.html-b253800d.js rename to assets/data_driver.html-52b7c40f.js index 6a5eb6c..0e9bff0 100644 --- a/assets/data_driver.html-b253800d.js +++ b/assets/data_driver.html-52b7c40f.js @@ -1,4 +1,4 @@ -import{_ as e,r as p,o,c as i,a as s,b as n,d as c,e as a}from"./app-72107ff1.js";const l={},u=a(`

    数据驱动

    数据驱动是测试框架非常重要的功能之一,它可以有效的节约大量重复的测试代码。seldom针对该功能做强大的支持。

    @class_data() 方法

    class_data() 装饰测试类,测试类下面的任何方法可以共用 class_data() 中定义的变量。

    • 用法一
    import seldom
    +import{_ as e,r as p,o,c as i,a as s,b as n,d as c,e as a}from"./app-9fb6f1b5.js";const l={},u=a(`

    数据驱动

    数据驱动是测试框架非常重要的功能之一,它可以有效的节约大量重复的测试代码。seldom针对该功能做强大的支持。

    @class_data() 方法

    class_data() 装饰测试类,测试类下面的任何方法可以共用 class_data() 中定义的变量。

    • 用法一
    import seldom
     from seldom import data_class
     
     
    diff --git a/assets/data_driver.html-f8fd9d76.js b/assets/data_driver.html-f8fd9d76.js
    deleted file mode 100644
    index ffdba45..0000000
    --- a/assets/data_driver.html-f8fd9d76.js
    +++ /dev/null
    @@ -1,350 +0,0 @@
    -import{_ as e,r as p,o,c as i,a as s,b as n,d as c,e as a}from"./app-a06a2d51.js";const l={},u=a(`

    数据驱动

    数据驱动是测试框架非常重要的功能之一,它可以有效的节约大量重复的测试代码。seldom针对该功能做强大的支持。

    @class_data() 方法

    class_data() 装饰测试类,测试类下面的任何方法可以共用 class_data() 中定义的变量。

    • 用法一
    import seldom
    -from seldom import data_class
    -
    -
    -@data_class([
    -    {"username": "user_1", "password": "abc123"},
    -    {"username": "user_2", "password": "abc456"},
    -])
    -class DDTTest(seldom.TestCase):
    -
    -    def test_data_func(self):
    -        """ data driver case """
    -        print("username->", self.username)
    -        print("password->", self.password)
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(debug=True)
    -
    • 用法二
    import seldom
    -from seldom import data_class
    -
    -
    -@data_class(("username", "password"), [
    -    ("user_1", "abc123"),
    -    ("user_1", "abc456"),
    -])
    -class DDTTest(seldom.TestCase):
    -
    -    def test_data_func(self):
    -        """ data driver case """
    -        print("username->", self.username)
    -        print("password->", self.password)
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(debug=True)
    -

    @data()方法

    当测试数据量比较少的情况下,可以通过@data()管理测试数据。

    参数化测试用例

    
    -import seldom
    -from seldom import data
    -
    -
    -class DataDriverTest(seldom.TestCase):
    -
    -    @data([
    -        ("First case", "seldom"),
    -        ("Second case", "selenium"),
    -        ("Third case", "unittest"),
    -    ])
    -    def test_tuple_data(self, name, keyword):
    -        """
    -        Used tuple test data
    -        :param name: case desc
    -        :param keyword: case data
    -        """
    -        print(f"test data: {keyword}")
    -
    -    @data([
    -        ["First case", "seldom"],
    -        ["Second case", "selenium"],
    -        ["Third case", "unittest"],
    -    ])
    -    def test_list_data(self, name, keyword):
    -        """
    -        Used list test data
    -        """
    -        print(f"test data: {keyword}")
    -
    -    @data([
    -        {"scene": 'First case', 'keyword': 'seldom'},
    -        {"scene": 'Second case', 'keyword': 'selenium'},
    -        {"scene": 'Third case', 'keyword': 'unittest'},
    -    ])
    -    def test_dict_data(self, scene, keyword):
    -        """
    -        used dict test data
    -        """
    -        print(f"case desc: {scene}")
    -        print(f"test data: {keyword}")
    -
    -    @data([
    -        [1, 2], [3, 4], [5, 6]
    -    ],
    -        cartesian=True)
    -    def test_cartesian_product(self, one, two, three):
    -        """
    -        cartesian product
    -        """
    -        print(f"test data: {one}, {two}, {three}")
    -
    -

    通过@data() 装饰器来参数化测试用例。

    动态生成测试数据

    除了使用固定的数据外,也可以动态生成一些测试数据用于自动化测试。

    import seldom
    -from seldom import data
    -from seldom import testdata
    -
    -
    -def test_data() -> list:
    -    """
    -    自动生成测试数据
    -    return [{},{}]
    -    """
    -    login_data = []
    -    for i in range(5):
    -        login_data.append({
    -            "scene": f"login{i}",
    -            "username": testdata.get_email(),
    -            "password": testdata.get_int(100000, 999999)
    -        })
    -    return login_data
    -
    -
    -class MyTest(seldom.TestCase):
    -
    -    @data(test_data())
    -    def test_login(self, _, username, password):
    -        """test login"""
    -        print(f"test username: {username}")
    -        print(f"test password: {password}")
    -

    @file_data() 方法

    当测试数据量比较大的情况下,可以通过@file_data()管理测试数据。

    CSV 文件参数化

    seldom 支持将csv文件的参数化。

    表格内容如下(data.csv):

    usernamepassword
    adminadmin123
    guestguest123
    import seldom
    -from seldom import file_data
    -
    -
    -class YouTest(seldom.TestCase):
    -
    -    @file_data("data.csv", line=2, end_line=10)
    -    def test_login(self, username, password):
    -        """a simple test case """
    -        print(username)
    -        print(password)
    -        # ...
    -
    -
    • file: 指定 csv 文件的路径。
    • line: 指定从第几行开始读取,默认第 1 行。
    • end_line: 指定读取到第几行的数据,默认None, 最后一行。

    excel 文件参数化

    seldom 支持将excel文件的参数化。

    import seldom
    -from seldom import file_data
    -
    -
    -class YouTest(seldom.TestCase):
    -
    -    @file_data("data.xlsx", sheet="Sheet1", line=2, end_line=10)
    -    def test_login(self, username, password):
    -        """a simple test case """
    -        print(username)
    -        print(password)
    -        # ...
    -
    -
    • file : 指定 excel 文件的路径。
    • sheet: 指定 excel 的标签页,默认名称为 Sheet1。
    • line : 指定从第几行开始读取,默认第 1 行。
    • end_line: 指定读取到第几行的数据,默认None, 最后一行。

    JSON 文件参数化

    seldom 支持将JSON文件的参数化。

    json 文件:

    {
    -  "login1": [
    -    [
    -      "admin",
    -      "admin123"
    -    ],
    -    [
    -      "guest",
    -      "guest123"
    -    ]
    -  ],
    -  "login2": [
    -    {
    -      "username": "Tom",
    -      "password": "tom123"
    -    },
    -    {
    -      "username": "Jerry",
    -      "password": "jerry123"
    -    }
    -  ]
    -}
    -

    注:login1login2 的调用方法一样。 区别是前者更简洁,后者更易读。

    import seldom
    -from seldom import file_data
    -
    -
    -class YouTest(seldom.TestCase):
    -
    -    @file_data("data.json", key="login1")
    -    def test_login(self, username, password):
    -        """a simple test case """
    -        print(username)
    -        print(password)
    -        # ...
    -
    -
    • file : 指定 JSON 文件的路径。
    • key: 指定字典的 key,默认不指定解析整个 JSON 文件。

    YAML 文件参数化

    seldom 支持YAML文件的参数化。

    data.yaml 文件:

    login1:
    -  - - admin
    -    - admin123
    -  - - guest
    -    - guest123
    -login2:
    -  - username: Tom
    -    password: tom123
    -  - username: Jerry
    -    password: jerry123
    -

    JSON用法一样,YAML书写更加简洁。

    import seldom
    -from seldom import file_data
    -
    -
    -class YouTest(seldom.TestCase):
    -
    -    @file_data("data.yaml", key="login1")
    -    def test_login(self, username, password):
    -        """a simple test case """
    -        print(username)
    -        print(password)
    -        # ...
    -
    -
    • file : 指定 YAML 文件的路径。
    • key: 指定字典的 key,默认不指定解析整个 YAML 文件。

    解释: @file_data()是如何查找测试数据文件的?

    mypro/
    -├── test_dir/
    -│   ├── module/
    -│   │   ├── case/
    -│   │   │   ├── test_sample.py (使用@file_data)
    -├── test_data/
    -│   ├── module_data/
    -│   │   ├── data.csv (测试数据文件所以位置)
    -...
    -

    test_sample.py 中使用@file_data("data.csv")默认只能向上查找两级目录,即到module目录下遍历查找data.csv 文件。显然这中情况下是无法找到data.csv 文件的。

    如果用例层级比较深,只需要指定文件目录的“相对路径”即可,使用方式:@file_data("test_data/module_data/data.csv") ,不要加./的前缀。

    支持配置测试环境

    在自动化测试过程中,我们往往需要一套代码在不同的环境下运行,seldom支持根据环境使用不同的数据文件。

    • 数据文件目录结构(一)
    .
    -└── test_data
    -    ├── develop
    -    │   └── test_data.json
    -    ├── product
    -    │   └── test_data.json
    -    └── test
    -        └── test_data.json
    -
    • 数据文件目录结构(二)
    .
    -├── develop
    -│   └── test_data
    -│       └── test_data.json
    -├── product
    -│   └── test_data
    -│       └── test_data.json
    -└── test
    -    └── test_data
    -        └── test_data.json
    -
    • 配置测试环境
    import seldom
    -from seldom import file_data
    -from seldom import Seldom
    -
    -
    -class MyTest(seldom.TestCase):
    -
    -    # 数据文件目录结构(一)
    -    @file_data("test_data.json")
    -    def test_case(self, req, resp):
    -        f"""a simple test case"""
    -        ...
    -
    -    # 数据文件目录结构(二)
    -    @file_data("test_data/test_data.json")
    -    def test_case(self, req, resp):
    -        f"""a simple test case"""
    -        ...
    -
    -
    -if __name__ == '__main__':
    -    Seldom.env = "product"  # test/develop/product 设置当前环境
    -    seldom.main(debug=True)
    -

    Seldom.env 默认为None,当设置了环境,@file_data() 会带上环境的目录名,例如:

    • test_data.json 查找的文件为 product/test_data.json
    • test_data/test_data.json 查找的文件为 product/test_data/test_data.json

    Seldom.env 可以随意命名,但最好遵循一定的规范:test/develop/product。你还可以利用Seldom.env变量本地创建更多的配置。

    @api_data()方法

    越来越多的公司落地 数据工厂,通过造数平台/数据工厂 来创建管理测试数据;@api_data() 装饰器支持通过URL获取驱动数据。

    • 接口数据

    http://127.0.0.1:8080/v1/public/data_service/get_case_data?data_id=1

    {
    -  "success": true,
    -  "error": {
    -    "code": "",
    -    "message": ""
    -  },
    -  "result": [
    -    {
    -      "scene": "测试1",
    -      "email": "li123@126.com",
    -      "password": "abc123"
    -    },
    -    {
    -      "scene": "测试2",
    -      "email": "li456@126.com",
    -      "password": "abc456"
    -    }
    -  ]
    -}
    -
    • 调用接口数据
    import seldom
    -from seldom import api_data
    -
    -
    -class TestApi(seldom.TestCase):
    -
    -    @api_data(url="http://127.0.0.1:8080/v1/public/data_service/get_case_data",
    -              params={"data_id": 1},
    -              headers={"X-Account-Email": "li.li@gmail.com"},
    -              ret="result")
    -    def test_case(self, scene, email, password):
    -        """
    -        test get request
    -        """
    -        print("name:", scene)
    -        print("email:", email)
    -        print("password:", password)
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(debug=True)
    -

    api_data()参数说明

    • url: 返回数据的接口url地址;默认仅支持GET 接口。
    • params: 请求参数。
    • header: 请求头。
    • ret: 提取接口返回的数据,默认仅支持 list 类型。

    使用函数构造数据

    如果数据驱动使用的数据比较简单其有规律,可以通过自定义函数生成,并且把函数传给 @data() 装饰器即可。

    import seldom
    -from seldom import data
    -
    -
    -def register():
    -    """生成注册账号信息"""
    -    users = []
    -    for i in range(10):
    -        users.append({
    -            "username": f"user{i}",
    -            "password": f"abc123{i}",
    -            "password2": f"abc123{i}"}
    -        )
    -
    -    return users
    -
    -
    -class DDTTest(seldom.TestCase):
    -
    -    @data(register())
    -    def test_data_func(self, username, password, password2):
    -        """ data driver case """
    -        print("username->", username)
    -        print("password->", password)
    -        print("password2->", password2)
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(debug=True)
    -

    支持第三方 ddt 库

    `,70),d={href:"https://github.com/datadriventests/ddt",target:"_blank",rel:"noopener noreferrer"},r=a(`

    安装:

    > pip install ddt
    -

    创建测试文件test_data.json

    {
    -  "test_data_1": {
    -    "word": "seldom"
    -  },
    -  "test_data_2": {
    -    "word": "unittest"
    -  },
    -  "test_data_3": {
    -    "word": "selenium"
    -  }
    -}
    -

    在 seldom 使用ddt

    import seldom
    -from ddt import ddt, file_data
    -
    -
    -@ddt
    -class YouTest(seldom.TestCase):
    -
    -    @file_data("test_data.json")
    -    def test_case(self, word):
    -        """a simple test case """
    -        self.open("https://www.baidu.com")
    -        self.type(id_="kw", text=word)
    -        self.click(css="#su")
    -        self.assertTitle(word + "_百度搜索")
    -
    -
    -if __name__ == '__main__':
    -    seldom.main()
    -

    更多的用法请查看 ddt 文档:https://ddt.readthedocs.io/en/latest/example.html

    `,7);function k(v,m){const t=p("ExternalLinkIcon");return o(),i("div",null,[u,s("p",null,[n("seldom 仍然允许你使用第三方参数化库,例如:"),s("a",d,[n("ddt"),c(t)]),n("。")]),r])}const q=e(l,[["render",k],["__file","data_driver.html.vue"]]);export{q as default}; diff --git a/assets/db_operation.html-8c3713ed.js b/assets/db_operation.html-43b02014.js similarity index 99% rename from assets/db_operation.html-8c3713ed.js rename to assets/db_operation.html-43b02014.js index b81e94f..4f14eb7 100644 --- a/assets/db_operation.html-8c3713ed.js +++ b/assets/db_operation.html-43b02014.js @@ -1,4 +1,4 @@ -import{_ as p,r as o,o as l,c as i,a as n,b as s,d as t,e}from"./app-a06a2d51.js";const c={},u=e(`

    数据库操作

    seldom 支持sqlite3、MySQL、SQL Server、MongoDB、PostgreSQL数据库操作。

    sqlite3MySQLSQL ServerPostgreSQL
    execute_sql()execute_sql()execute_sql()execute_sql()
    query_sql()query_sql()query_sql()query_sql()
    query_one()query_one()query_one()query_one()
    insert_get_last_id()insert_get_last_id()insert_get_last_id()insert_get_last_id()
    delete()delete()delete()delete()
    insert()insert()insert()insert()
    select()select()select()select()
    update()update()update()update()
    init_table()init_table()init_table()init_table()
    close()close()close()close()

    连接数据库

    连接sqlit3数据库

    from seldom.db_operation import SQLiteDB
    +import{_ as p,r as o,o as l,c as i,a as n,b as s,d as t,e}from"./app-9fb6f1b5.js";const c={},u=e(`

    数据库操作

    seldom 支持sqlite3、MySQL、SQL Server、MongoDB、PostgreSQL数据库操作。

    sqlite3MySQLSQL ServerPostgreSQL
    execute_sql()execute_sql()execute_sql()execute_sql()
    query_sql()query_sql()query_sql()query_sql()
    query_one()query_one()query_one()query_one()
    insert_get_last_id()insert_get_last_id()insert_get_last_id()insert_get_last_id()
    delete()delete()delete()delete()
    insert()insert()insert()insert()
    select()select()select()select()
    update()update()update()update()
    init_table()init_table()init_table()init_table()
    close()close()close()close()

    连接数据库

    连接sqlit3数据库

    from seldom.db_operation import SQLiteDB
     
     db = SQLiteDB(r"D:\\learnAPI\\db.sqlite3")
     

    连接MySQL数据库

    1. 安装pymysql驱动
    > pip install pymysql
    diff --git a/assets/db_operation.html-e0a4c7c7.js b/assets/db_operation.html-e0a4c7c7.js
    deleted file mode 100644
    index 3ded2a7..0000000
    --- a/assets/db_operation.html-e0a4c7c7.js
    +++ /dev/null
    @@ -1,64 +0,0 @@
    -import{_ as p,r as o,o as l,c as i,a as n,b as s,d as t,e}from"./app-72107ff1.js";const c={},u=e(`

    数据库操作

    seldom 支持sqlite3、MySQL、SQL Server、MongoDB、PostgreSQL数据库操作。

    sqlite3MySQLSQL ServerPostgreSQL
    execute_sql()execute_sql()execute_sql()execute_sql()
    query_sql()query_sql()query_sql()query_sql()
    query_one()query_one()query_one()query_one()
    insert_get_last_id()insert_get_last_id()insert_get_last_id()insert_get_last_id()
    delete()delete()delete()delete()
    insert()insert()insert()insert()
    select()select()select()select()
    update()update()update()update()
    init_table()init_table()init_table()init_table()
    close()close()close()close()

    连接数据库

    连接sqlit3数据库

    from seldom.db_operation import SQLiteDB
    -
    -db = SQLiteDB(r"D:\\learnAPI\\db.sqlite3")
    -

    连接MySQL数据库

    1. 安装pymysql驱动
    > pip install pymysql
    -
    1. 链接
    from seldom.db_operation import MySQLDB
    -
    -db = MySQLDB(host="127.0.0.1",
    -             port=3306,
    -             user="root",
    -             password="123",
    -             database="db_name")
    -

    连接SQL Server数据库(需要)

    1. 安装pymssql驱动
    > pip install pymssql
    -
    1. 链接
    from seldom.db_operation.mssql_db import MSSQLDB
    -
    -db = MSSQLDB(server="127.0.0.1",
    -             user="SA",
    -             password="tc@123",
    -             database="TestDB")
    -

    操作方法

    • execute_sql

    执行sql语句,无返回结果。

    db.execute_sql("INSERT INTO user (id, name) VALUES (1, 'tom') ")
    -db.execute_sql("UPDATE user SET name = 'jack' WHERE id=1")
    -db.execute_sql("DELETE FROM user WHERE id = 1")
    -
    • query_sql

    执行查询sql语句,返回查询结果。

    ret = db.query_sql("select * from user")
    -print(ret)
    -
    • query_one

    执行查询sql语句,返回一条结果。

    ret = db.query_one("select * from user")
    -print(ret)
    -
    • insert_get_last_id

    插入数据并返回最新的ID。

    last_id = db.insert_get_last_id("INSERT INTO user (id, name) VALUES (1, 'tom') ")
    -print(last_id)
    -
    • delete

    删除表数据。

    db.delete(table="user", where={"id": 1})
    -
    • insert

    插入一条数据。

    data = {"id": 10, "name": "jean"}
    -db.insert(table="user", data=data)
    -
    • select

    查询表数据。

    result = db.select(table="user", where={"id": 1, "name": "tom"})
    -print(result)
    -result = db.select(table="user", one=True)  # one=True 返回一条结果
    -print(result)
    -
    • update

    更新表数据。

    db.update(table="user", where={"name": "tom", }, data={"name": "jack"})
    -
    • init_table

    批量插入数据,在插入之前先清空表数据。

    
    -# more table data
    -table_data = {
    -    "group": [
    -        {"id": 1, "name": "test"},
    -        {"id": 2, "name": "product"},
    -        {"id": 3, "name": "develop"},
    -    ],
    -    "user": [
    -        {"id": 1, "name": "jeannie"},
    -        {"id": 2, "name": "joye"},
    -        {"id": 3, "name": "blue"},
    -    ],
    -
    -}
    -
    -db.init_table(table_data)
    -
    • close

    关闭数据库连接。

    db.close()
    -

    MongoDB

    MongoDB 是一个基于分布式文件存储的数据库,属于非关系型数据库,与关系型数据库得操作有着较大得差异,它本身支持字典传参,所以,seldom 只简单封装了数据库连接。

    • 安装pymongo

    https://github.com/mongodb/mongo-python-driver

    > pip show pymongo
    -
    • 连接MongoDB
    from seldom.db_operation.mongo_db import MongoDB
    -
    -db = MongoDB(host="localhost", port=27017, db="yapi")
    -

    参数说明:

    • host: 连接地址。
    • port: 端口号。
    • db: 数据库名字。

    pymongo

    `,57),r={href:"https://github.com/mongodb/mongo-python-driver",target:"_blank",rel:"noopener noreferrer"},d=e(`
    • 获取集合信息
    col = db.list_collection_names()
    -print(col)
    -

    结果:

    collection list:  ['project', 'log', ...]
    -
    • 获取表一条数据
    data = db.project.find_one()
    -print("table one data:", data)
    -

    结果:

    table data: {'_id': 11, 'switch_notice': True, 'is_mock_open': False, 'strice': False, 'is_json5': False, 'name': '发布会签到系统'}
    -
    `,8),k={href:"https://www.runoob.com/python3/python-mongodb-insert-document.html",target:"_blank",rel:"noopener noreferrer"},m={href:"https://www.runoob.com/python3/python-mongodb-query-document.html",target:"_blank",rel:"noopener noreferrer"},v={href:"https://www.runoob.com/python3/python-mongodb-update-document.html",target:"_blank",rel:"noopener noreferrer"},b={href:"https://www.runoob.com/python3/python-mongodb-delete-document.html",target:"_blank",rel:"noopener noreferrer"};function g(h,q){const a=o("ExternalLinkIcon");return l(),i("div",null,[u,n("p",null,[s("以下操作seldom没有做任何封装,请参考"),n("a",r,[s("pymongo"),t(a)])]),d,n("ul",null,[n("li",null,[n("a",k,[s("添加数据"),t(a)])]),n("li",null,[n("a",m,[s("查询数据"),t(a)])]),n("li",null,[n("a",v,[s("修改数据"),t(a)])]),n("li",null,[n("a",b,[s("删除数据"),t(a)])])])])}const _=p(c,[["render",g],["__file","db_operation.html.vue"]]);export{_ as default}; diff --git a/assets/dependent_func.html-974e3a57.js b/assets/dependent_func.html-3403aa6e.js similarity index 99% rename from assets/dependent_func.html-974e3a57.js rename to assets/dependent_func.html-3403aa6e.js index 48ab47c..adc6596 100644 --- a/assets/dependent_func.html-974e3a57.js +++ b/assets/dependent_func.html-3403aa6e.js @@ -1,4 +1,4 @@ -import{_ as t,r as o,o as p,c,a as n,b as s,d as i,e as a}from"./app-72107ff1.js";const l={},u=a('

    方法的依赖

    在 seldom 3.4.0 版本实现了该功能。

    在复杂的测试场景中,常常会存在用例依赖,以一个接口自动化平台为例,依赖关系:

    创建用例 --> 创建模块 --> 创建项目 --> 登录

    用例依赖的问题

    • 用例的依赖对于的执行顺序有严格的要求,比如让被依赖的方法先执行。
    • 一旦使用用例依赖,依赖的用例就无法单独执行了,按照用例的设计原则,每条用例都应该独立执行。

    正确的做法

    我们应该将依赖的操作封装成方法调用。如果能通过装饰器实现调用,那就很有趣了。

    ',8),d={href:"https://github.com/ae86sen/aomaker",target:"_blank",rel:"noopener noreferrer"},r=a(`

    类内部方法调用

    我们可以在测试类下面,创建普通的方法。然后通过@dependent_func()装饰器调用他。

    import seldom
    +import{_ as t,r as o,o as p,c,a as n,b as s,d as i,e as a}from"./app-9fb6f1b5.js";const l={},u=a('

    方法的依赖

    在 seldom 3.4.0 版本实现了该功能。

    在复杂的测试场景中,常常会存在用例依赖,以一个接口自动化平台为例,依赖关系:

    创建用例 --> 创建模块 --> 创建项目 --> 登录

    用例依赖的问题

    • 用例的依赖对于的执行顺序有严格的要求,比如让被依赖的方法先执行。
    • 一旦使用用例依赖,依赖的用例就无法单独执行了,按照用例的设计原则,每条用例都应该独立执行。

    正确的做法

    我们应该将依赖的操作封装成方法调用。如果能通过装饰器实现调用,那就很有趣了。

    ',8),d={href:"https://github.com/ae86sen/aomaker",target:"_blank",rel:"noopener noreferrer"},r=a(`

    类内部方法调用

    我们可以在测试类下面,创建普通的方法。然后通过@dependent_func()装饰器调用他。

    import seldom
     from seldom.testdata import get_md5
     from seldom.utils import cache, dependent_func
     
    diff --git a/assets/dependent_func.html-bd1f5d4f.js b/assets/dependent_func.html-bd1f5d4f.js
    deleted file mode 100644
    index 3fbbbfe..0000000
    --- a/assets/dependent_func.html-bd1f5d4f.js
    +++ /dev/null
    @@ -1,193 +0,0 @@
    -import{_ as t,r as o,o as p,c,a as n,b as s,d as i,e as a}from"./app-a06a2d51.js";const l={},u=a('

    方法的依赖

    在 seldom 3.4.0 版本实现了该功能。

    在复杂的测试场景中,常常会存在用例依赖,以一个接口自动化平台为例,依赖关系:

    创建用例 --> 创建模块 --> 创建项目 --> 登录

    用例依赖的问题

    • 用例的依赖对于的执行顺序有严格的要求,比如让被依赖的方法先执行。
    • 一旦使用用例依赖,依赖的用例就无法单独执行了,按照用例的设计原则,每条用例都应该独立执行。

    正确的做法

    我们应该将依赖的操作封装成方法调用。如果能通过装饰器实现调用,那就很有趣了。

    ',8),d={href:"https://github.com/ae86sen/aomaker",target:"_blank",rel:"noopener noreferrer"},r=a(`

    类内部方法调用

    我们可以在测试类下面,创建普通的方法。然后通过@dependent_func()装饰器调用他。

    import seldom
    -from seldom.testdata import get_md5
    -from seldom.utils import cache, dependent_func
    -
    -
    -class DependentTest(seldom.TestCase):
    -
    -    @staticmethod
    -    def user_login(username, password):
    -        """
    -        模拟用户登录,获取登录token
    -        """
    -        return get_md5(username+password)
    -
    -    @dependent_func(user_login, username="tom", password="t123")
    -    def test_case(self,):
    -        """
    -        sample test case
    -        """
    -        token = cache.get("user_login")
    -        print("token", token)
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(debug=True)
    -    cache.clear()
    -

    说明

    这个例子涉及到不少知识点。

    1. test_case() 用例依赖 user_login() 方法,通过 @dependent_func() 装饰器调用 user_login 方法。
    2. user_login() 方法运行的时候需要参数(username、password),可以直接在 @dependent_func() 装饰器中设置参数:username="tom"password="t123"
    3. user_login() 方法运行运行完会生成 token, 保存于 cache中,通过 cache.get() 可以获取到token, 默认通过方法名user_login 作为key获取。
    4. 为了简化代码,生成token 是通过 get_md5() 根据传入的参数生成的一个 md5 值。
    5. cache.clear() 用于清空缓存, 再次调用 user_login() 方法直接不执行,应为cache已经有上次的执行结果了。

    执行日志

    python zzz_demo.py
    -...
    -test_case (zzz_demo.DependentTest.test_case)
    -sample test case ... 2023-11-15 23:26:36 | INFO     | dependence.py | 🔗 <test_case> depends on <user_login>, execute.
    -2023-11-15 23:26:36 | INFO     | cache.py | 💾 Set cache data: user_login = 35e0ff9c4cba89998dda8255d0eb5408
    -2023-11-15 23:26:36 | INFO     | cache.py | 💾 Get cache data: user_login = 35e0ff9c4cba89998dda8255d0eb5408
    -token 35e0ff9c4cba89998dda8255d0eb5408
    -ok
    -
    -----------------------------------------------------------------------
    -Ran 1 test in 0.005s
    -
    -OK
    -2023-11-15 23:26:36 | SUCCESS  | runner.py | A run the test in debug mode without generating HTML report!
    -2023-11-15 23:26:36 | INFO     | cache.py | 💾 Clear all cache data
    -

    外部类方法依赖

    • 创建依赖方法
    # common.py
    -from seldom.testdata import get_md5
    -
    -
    -class Login:
    -
    -    @staticmethod
    -    def account_login(username, password):
    -        """
    -        模拟用户&密码登录,获取登录token
    -        """
    -        return get_md5(username+password)
    -
    -
    -login=Login()
    -
    • 用例引用依赖方法
    import seldom
    -from seldom.utils import cache, dependent_func
    -from common import Login # 方式1:引用依赖类
    -# from common import login  # 方式2:引用初始化好的类对象
    -
    -
    -class DependentTest(seldom.TestCase):
    -
    -
    -    @dependent_func(Login().account_login, key_name="token1", username="tom", password="t123")
    -    # @dependent_func(login.account_login, key_name="token1", username="tom", password="t123")
    -    def test_case(self):
    -        """
    -        Used tuple test data
    -        """
    -        token = cache.get("token1")
    -        print("token", token)
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(debug=True)
    -

    说明

    1. Common 类的account_login()方法可以不设置为静态方法,导入时需要类需要加括号:Common().user_login。 或者先初始化类对象login=Login() 再调用。
    2. key_name 指定缓存的 key,如果指定为token1, 从缓存读取也使用这个cache.get("token1")

    多重方法依赖

    复杂的场景当然是需要多重依赖的。

    1. 被依赖的方法可以进一步使用 dependent_func()装饰器进行多重复依赖。

    查询模块 --> 查询项目 --> 登录

    # common.py
    -from seldom.testdata import get_md5, get_int
    -from seldom.utils import cache, dependent_func
    -
    -class Login:
    -
    -    @staticmethod
    -    def account_login(username, password):
    -        """
    -        模拟用户&密码登录,获取登录token
    -        """
    -        return get_md5(username+password)
    -
    -class DepFunc:
    -
    -    @staticmethod
    -    @dependent_func(Login.account_login, key_name="token", username="jack", password="456")
    -    def get_project_id():
    -        token = cache.get("token")
    -        print(f"使用token:{token} 查询项目, 返回项目ID")
    -        return get_int(1, 1000)
    -
    -
    -    @staticmethod
    -    @dependent_func(get_project_id, key_name="pid")
    -    def get_module_id():
    -        pid = cache.get("pid")
    -        print(f"使用项目ID:{pid} 查询模块, 返回模块ID")
    -        return get_int(1, 1000)
    -

    在用例中直接调用 DepFunc.get_module_id 方法即可。

    import seldom
    -from seldom.utils import cache, dependent_func
    -from common import DepFunc
    -
    -
    -class DependentTest(seldom.TestCase):
    -
    -
    -    @dependent_func(DepFunc.get_module_id, key_name="mid")
    -    def test_case(self):
    -        """
    -        sample test case
    -        """
    -        mid = cache.get("mid")
    -        print(f"模块ID: {mid}")
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(debug=True)
    -    cache.clear()
    -
    1. 测试用例也可以同时使用多个 @dependent_func() 装饰器依赖多个方法,顺序由上到下执行,这种方式主要用于被依赖的方法之间没有依赖关系。
    # common.py
    -from seldom.testdata import get_int, username
    -
    -
    -class DataFunc:
    -
    -    @staticmethod
    -    def get_name():
    -        return username(language="zh")
    -
    -    @staticmethod
    -    def get_age():
    -        return get_int(1, 99)
    -

    在用例中使用多个@dependent_func()依赖装饰器。

    import seldom
    -from seldom.utils import cache, dependent_func
    -from common import DataFunc
    -
    -
    -class DependentTest(seldom.TestCase):
    -
    -
    -    @dependent_func(DataFunc.get_name, key_name="name")
    -    @dependent_func(DataFunc.get_age, key_name="age")
    -    def test_case(self):
    -        """
    -        sample test case
    -        """
    -        name = cache.get("name")
    -        age = cache.get("age")
    -        print(f"名字: {name}, 年龄: {age}")
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(debug=True)
    -    cache.clear()
    -

    参数化使用

    参数化 @data()@file_data() 是seldom最重要的功能之一,能否和 @dependent_func() 一起使用? 当然可以。

    import seldom
    -from seldom import data
    -from seldom.testdata import get_md5
    -from seldom.utils import cache, dependent_func
    -
    -
    -class DependentTest(seldom.TestCase):
    -
    -    @staticmethod
    -    def user_login(username, password):
    -        """
    -        模拟用户登录,获取登录token
    -        """
    -        return get_md5(username+password)
    -
    -    @data([
    -        ("case1", "foo"),
    -        ("case2", "bar"),
    -    ])
    -    @dependent_func(user_login, username="tom", password="t123")
    -    def test_case(self, _, keyword):
    -        """
    -        Used tuple test data
    -        """
    -        token = cache.get("user_login")
    -        print(keyword, "token", token)
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(debug=True)
    -    cache.clear()
    -

    说明

    1. @data() 装饰器必须写在 @dependent_func() 的上面。
    2. 运行两条用例,user_login() 被执行过一次后,第二次则不需要重复执行,直接返回结果。
    `,31);function k(m,v){const e=o("ExternalLinkIcon");return p(),c("div",null,[u,n("p",null,[n("a",d,[s("aomaker"),i(e)]),s(" 提供了这种装饰器的实现,seldom 进行了复刻,只是的定位和用法用有所不同。")]),r])}const g=t(l,[["render",k],["__file","dependent_func.html.vue"]]);export{g as default}; diff --git a/assets/develop.html-5979c2ed.js b/assets/develop.html-5979c2ed.js deleted file mode 100644 index ccc98e5..0000000 --- a/assets/develop.html-5979c2ed.js +++ /dev/null @@ -1,24 +0,0 @@ -import{_ as l,r as t,o,c as r,a as e,b as n,d as a,e as i}from"./app-a06a2d51.js";const d={},c=e("h2",{id:"☘️introduction",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#☘️introduction","aria-hidden":"true"},"#"),n(" ☘️Introduction")],-1),p={href:"https://seldomqa.github.io/",target:"_blank",rel:"noopener noreferrer"},u=i(`

    你可以使用 Markdown 书写文档,并通过 VuePress 部署为可预览的页面。

    📖使用说明

    1. 安装

    1. clone本项目并安装依赖
    git clone https://github.com/SeldomQA/seldom.git
    -cd docs
    -yarn install
    -

    2. 开发

    `,6),h={href:"https://v2.vuepress.vuejs.org/zh/",target:"_blank",rel:"noopener noreferrer"},v=i(`

    docs/vpdocs文件夹内,修改你想修改的.md文档并保存。

    然后执行以下命令进行预览或打包

    yarn run dev # 预览
    -yarn run build # 生成静态页面
    -

    部署

    Github-Pages手动本地部署部署说明:

    本地进入项目中执行deploy.sh即可自动部署到github pages。

    deploy.sh 的详情如下(请自行判断启用注释掉的命令):

    #!/usr/bin/env sh
    -# 确保脚本抛出遇到的错误
    -set -e
    -
    -# 生成静态文件
    -npm run build
    -
    -# 进入生成的文件夹
    -cd vpdocs/.vuepress/dist
    -
    -git init
    -git add -A
    -git commit -m 'deploy'
    -
    -# 如果发布到 https://SeldomQA.github.io
    -git push -f git@github.com:SeldomQA/SeldomQA.github.io.git master
    -
    -cd -
    -
    `,8),m={href:"https://v1.vuepress.vuejs.org/guide/deploy.html",target:"_blank",rel:"noopener noreferrer"},b=e("hr",null,null,-1),g={href:"https://github.com/nickliya",target:"_blank",rel:"noopener noreferrer"};function _(k,f){const s=t("ExternalLinkIcon");return o(),r("div",null,[c,e("p",null,[n("基于 vuepress2.0+ 的 "),e("strong",null,[n("seldom "),e("a",p,[n("操作文档"),a(s)])])]),u,e("p",null,[n("正式开发前,可以先阅读 "),e("a",h,[n("VuePress官方文档"),a(s)]),n("。")]),v,e("p",null,[n("更多部署方式可以参阅 "),e("a",m,[n("VuePress文档|部署"),a(s)]),n("。")]),b,e("p",null,[n("Author:"),e("a",g,[n("@Yongchin"),a(s)])])])}const y=l(d,[["render",_],["__file","develop.html.vue"]]);export{y as default}; diff --git a/assets/develop.html-45b4d86e.js b/assets/develop.html-ecb59dad.js similarity index 98% rename from assets/develop.html-45b4d86e.js rename to assets/develop.html-ecb59dad.js index 877b5a0..ab689ba 100644 --- a/assets/develop.html-45b4d86e.js +++ b/assets/develop.html-ecb59dad.js @@ -1,4 +1,4 @@ -import{_ as l,r as t,o,c as r,a as e,b as n,d as a,e as i}from"./app-72107ff1.js";const d={},c=e("h2",{id:"☘️introduction",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#☘️introduction","aria-hidden":"true"},"#"),n(" ☘️Introduction")],-1),p={href:"https://seldomqa.github.io/",target:"_blank",rel:"noopener noreferrer"},u=i(`

    你可以使用 Markdown 书写文档,并通过 VuePress 部署为可预览的页面。

    📖使用说明

    1. 安装

    1. clone本项目并安装依赖
    git clone https://github.com/SeldomQA/seldom.git
    +import{_ as l,r as t,o,c as r,a as e,b as n,d as a,e as i}from"./app-9fb6f1b5.js";const d={},c=e("h2",{id:"☘️introduction",tabindex:"-1"},[e("a",{class:"header-anchor",href:"#☘️introduction","aria-hidden":"true"},"#"),n(" ☘️Introduction")],-1),p={href:"https://seldomqa.github.io/",target:"_blank",rel:"noopener noreferrer"},u=i(`

    你可以使用 Markdown 书写文档,并通过 VuePress 部署为可预览的页面。

    📖使用说明

    1. 安装

    1. clone本项目并安装依赖
    git clone https://github.com/SeldomQA/seldom.git
     cd docs
     yarn install
     

    2. 开发

    `,6),h={href:"https://v2.vuepress.vuejs.org/zh/",target:"_blank",rel:"noopener noreferrer"},v=i(`

    docs/vpdocs文件夹内,修改你想修改的.md文档并保存。

    然后执行以下命令进行预览或打包

    yarn run dev # 预览
    diff --git a/assets/extensions.html-2fc302d1.js b/assets/extensions.html-1d1fb4fe.js
    similarity index 76%
    rename from assets/extensions.html-2fc302d1.js
    rename to assets/extensions.html-1d1fb4fe.js
    index 28e0d3a..0b3b672 100644
    --- a/assets/extensions.html-2fc302d1.js
    +++ b/assets/extensions.html-1d1fb4fe.js
    @@ -1 +1 @@
    -const e=JSON.parse('{"key":"v-483ce5fe","path":"/app-testing/extensions.html","title":"appium 扩展","lang":"en-US","frontmatter":{},"headers":[{"level":2,"title":"appium images-plugin","slug":"appium-images-plugin","link":"#appium-images-plugin","children":[]},{"level":2,"title":"Appium OCR plugin","slug":"appium-ocr-plugin","link":"#appium-ocr-plugin","children":[]}],"git":{"updatedTime":1725890482000,"contributors":[{"name":"defnngj","email":"defnngj@gmail.com","commits":3}]},"filePathRelative":"app-testing/extensions.md"}');export{e as data};
    +const e=JSON.parse('{"key":"v-483ce5fe","path":"/app-testing/extensions.html","title":"appium 扩展","lang":"en-US","frontmatter":{},"headers":[{"level":2,"title":"appium images-plugin","slug":"appium-images-plugin","link":"#appium-images-plugin","children":[]},{"level":2,"title":"Appium OCR plugin","slug":"appium-ocr-plugin","link":"#appium-ocr-plugin","children":[]}],"git":{"updatedTime":1734494711000,"contributors":[{"name":"defnngj","email":"defnngj@gmail.com","commits":4}]},"filePathRelative":"app-testing/extensions.md"}');export{e as data};
    diff --git a/assets/extensions.html-58895b7c.js b/assets/extensions.html-6002e3f6.js
    similarity index 63%
    rename from assets/extensions.html-58895b7c.js
    rename to assets/extensions.html-6002e3f6.js
    index 633cbcf..bfe2912 100644
    --- a/assets/extensions.html-58895b7c.js
    +++ b/assets/extensions.html-6002e3f6.js
    @@ -1,8 +1,8 @@
    -import{_ as n,o as s,c as a,e as p}from"./app-72107ff1.js";const t={},o=p(`

    appium 扩展

    appium支持扩展,通过扩展来增强appium定位元素的能力。

    appium images-plugin

    使用此插件支持的-image定位器策略,可以通过Appium指定想要定位的元素的图片文件。

    • 安装Appium images-plugin插件。
    > appium plugin install images
    +import{_ as n,o as s,c as a,e as p}from"./app-9fb6f1b5.js";const t={},e=p(`

    appium 扩展

    appium支持扩展,通过扩展来增强appium定位元素的能力。

    appium images-plugin

    使用此插件支持的-image定位器策略,可以通过Appium指定想要定位的元素的图片文件。

    • 安装Appium images-plugin插件。
    > appium plugin install images
     
    • 查看已安装的Appium插件。
    > appium plugin list --installed
     ✔ Listing installed plugins
     - images@2.1.8 [installed (npm)]
    -
    • 启动Appium server时指定使用OCR插件。
    > appium server --address '127.0.0.1' -p 4723  --use-plugins=iamges
    +
    • 启动Appium server时指定使用OCR插件。
    > appium server --address '127.0.0.1' -p 4723  --use-plugins=images
     
    • 目录结构
    ├───test_appium_images.py
     └───phone.jpg
     
    • 编写App自动化测试脚本
    # test_appium_images.py
    @@ -65,45 +65,99 @@ import{_ as n,o as s,c as a,e as p}from"./app-72107ff1.js";const t={},o=p(`

    根据上面代码示例,打印ocr变量得到一个JSON结构体。

    {
       "words": [
         {
    -      "text": "mEngine", "confidence": 88.47775268554688,
    -      "bbox": {"x0": 86, "y0": 509, "x1": 308, "y1": 560}
    +      "text": "mEngine",
    +      "confidence": 88.47775268554688,
    +      "bbox": {
    +        "x0": 86,
    +        "y0": 509,
    +        "x1": 308,
    +        "y1": 560
    +      }
         },
         {
    -      "text": "Flyme", "confidence": 91.3454818725586,
    -       "bbox": {"x0": 316, "y0": 1132, "x1": 420, "y1": 1172}
    -      },
    +      "text": "Flyme",
    +      "confidence": 91.3454818725586,
    +      "bbox": {
    +        "x0": 316,
    +        "y0": 1132,
    +        "x1": 420,
    +        "y1": 1172
    +      }
    +    },
         {
    -      "text": "A9", "confidence": 34.86248779296875,
    -      "bbox": {"x0": 1017, "y0": 2565, "x1": 1078, "y1": 2595}
    +      "text": "A9",
    +      "confidence": 34.86248779296875,
    +      "bbox": {
    +        "x0": 1017,
    +        "y0": 2565,
    +        "x1": 1078,
    +        "y1": 2595
    +      }
         }
       ],
       "lines": [
         {
    -      "text": "mEngine BY Ni0vEh 1 Bl\\n\\n", "confidence": 21.003677368164062,
    -     "bbox": {"x0": 86, "y0": 500, "x1": 674, "y1": 560}
    +      "text": "mEngine BY Ni0vEh 1 Bl\\n\\n",
    +      "confidence": 21.003677368164062,
    +      "bbox": {
    +        "x0": 86,
    +        "y0": 500,
    +        "x1": 674,
    +        "y1": 560
    +      }
         },
         {
    -      "text": "Flyme\\n\\n", "confidence": 91.3454818725586,
    -     "bbox": {"x0": 316, "y0": 1132, "x1": 420, "y1": 1172}
    +      "text": "Flyme\\n\\n",
    +      "confidence": 91.3454818725586,
    +      "bbox": {
    +        "x0": 316,
    +        "y0": 1132,
    +        "x1": 420,
    +        "y1": 1172
    +      }
         },
         {
    -      "text": "A9\\n", "confidence": 34.86248779296875,
    -      "bbox": {"x0": 1017, "y0": 2565, "x1": 1078, "y1": 2595}
    +      "text": "A9\\n",
    +      "confidence": 34.86248779296875,
    +      "bbox": {
    +        "x0": 1017,
    +        "y0": 2565,
    +        "x1": 1078,
    +        "y1": 2595
    +      }
         }
       ],
       "blocks": [
         {
    -      "text": "mEngine BY Ni0vEh 1 Bl\\n\\n", "confidence": 21.003677368164062,
    -       "bbox": {"x0": 86, "y0": 500, "x1": 674, "y1": 560}
    -      },
    +      "text": "mEngine BY Ni0vEh 1 Bl\\n\\n",
    +      "confidence": 21.003677368164062,
    +      "bbox": {
    +        "x0": 86,
    +        "y0": 500,
    +        "x1": 674,
    +        "y1": 560
    +      }
    +    },
         {
    -      "text": "Flyme\\n\\n", "confidence": 91.3454818725586,
    -      "bbox": {"x0": 316, "y0": 1132, "x1": 420, "y1": 1172}
    +      "text": "Flyme\\n\\n",
    +      "confidence": 91.3454818725586,
    +      "bbox": {
    +        "x0": 316,
    +        "y0": 1132,
    +        "x1": 420,
    +        "y1": 1172
    +      }
         },
         {
    -      "text": "A9\\n", "confidence": 34.86248779296875, 
    -      "bbox": {"x0": 1017, "y0": 2565, "x1": 1078, "y1": 2595}
    +      "text": "A9\\n",
    +      "confidence": 34.86248779296875,
    +      "bbox": {
    +        "x0": 1017,
    +        "y0": 2565,
    +        "x1": 1078,
    +        "y1": 2595
    +      }
         }
       ]
     }
    -

    JSON结构体说明:

    • wrods - Tesseract识别的单个单词的列表。

    • lines - Tesseract识别的文本行的列表。

    • blocks - Tesseract识别连续文本块的列表。

    每项都引用一个OCR对象,它们本身包含3个数据:

    • text:识别的文本。
    • confidence:Tesseract对于给定文本的OCR处理结果的置信度(范围在0到100之间)。
    • bbox:发现文本的边界框,边界框标记为x0、x1、y0和y1的值的对象。分别表文本的上下左右坐标位置,其中。这里,x0表示发现文本的左边x坐标,x1表示右边x坐标,y0表示上部y坐标,y1表示下部y坐标。
    `,30),e=[o];function c(l,u){return s(),a("div",null,e)}const r=n(t,[["render",c],["__file","extensions.html.vue"]]);export{r as default}; +

    JSON结构体说明:

    • wrods - Tesseract识别的单个单词的列表。

    • lines - Tesseract识别的文本行的列表。

    • blocks - Tesseract识别连续文本块的列表。

    每项都引用一个OCR对象,它们本身包含3个数据:

    • text:识别的文本。
    • confidence:Tesseract对于给定文本的OCR处理结果的置信度(范围在0到100之间)。
    • bbox:发现文本的边界框,边界框标记为x0、x1、y0和y1的值的对象。分别表文本的上下左右坐标位置,其中。这里,x0表示发现文本的左边x坐标,x1表示右边x坐标,y0表示上部y坐标,y1表示下部y坐标。
    `,30),o=[e];function i(l,c){return s(),a("div",null,o)}const r=n(t,[["render",i],["__file","extensions.html.vue"]]);export{r as default}; diff --git a/assets/extensions.html-6bce6958.js b/assets/extensions.html-6bce6958.js deleted file mode 100644 index 17dc9cb..0000000 --- a/assets/extensions.html-6bce6958.js +++ /dev/null @@ -1,109 +0,0 @@ -import{_ as n,o as s,c as a,e as p}from"./app-a06a2d51.js";const t={},o=p(`

    appium 扩展

    appium支持扩展,通过扩展来增强appium定位元素的能力。

    appium images-plugin

    使用此插件支持的-image定位器策略,可以通过Appium指定想要定位的元素的图片文件。

    • 安装Appium images-plugin插件。
    > appium plugin install images
    -
    • 查看已安装的Appium插件。
    > appium plugin list --installed
    -✔ Listing installed plugins
    -- images@2.1.8 [installed (npm)]
    -
    • 启动Appium server时指定使用OCR插件。
    > appium server --address '127.0.0.1' -p 4723  --use-plugins=iamges
    -
    • 目录结构
    ├───test_appium_images.py
    -└───phone.jpg
    -
    • 编写App自动化测试脚本
    # test_appium_images.py
    -import seldom
    -from seldom.utils.file_extend import file
    -from seldom.appium_lab.android import UiAutomator2Options
    -
    -
    -class TestApp(seldom.TestCase):
    -
    -    def test_app_images(self):
    -        self.wait(10)
    -        file_path = file.join(file.dir, "phone.jpg")
    -        self.click_image(file_path)
    -
    -
    -if __name__ == '__main__':
    -    capabilities = {
    -        "automationName": "UiAutomator2",
    -        "platformName": "Android",
    -        "appPackage": "com.meizu.flyme.flymebbs",
    -        "appActivity": "com.meizu.myplus.ui.splash.SplashActivity",
    -        "noReset": True,
    -    }
    -    options = UiAutomator2Options().load_capabilities(capabilities)
    -    seldom.main(app_server="http://127.0.0.1:4723", app_info=options)
    -

    通过click_image() 来点击图片匹配到整个页面上的元素的坐标位。

    Appium OCR plugin

    • 安装Appium OCR plugin插件。
    > appium plugin install images--source=npm appium-ocr-plugin
    -
    • 查看已安装的Appium插件。
    > appium plugin list --installed
    -✔ Listing installed plugins
    -- ocr@0.2.0 [installed (npm)]
    -
    • 启动Appium server时指定使用OCR插件。
    > appium server --address '127.0.0.1' -p 4723  --use-plugins=ocr
    -
    • 编写App自动测试脚本。
    # test_appium_orc.py
    -import seldom
    -from seldom.appium_lab.switch import Switch
    -from seldom.appium_lab.ocr_plugin import OCRCommand
    -from seldom.appium_lab.android import UiAutomator2Options
    -
    -
    -class TestApp(seldom.TestCase):
    -    def start(self):
    -        self.switch = Switch(self.driver)
    -
    -    def test_orc_case(self):
    -        ocr = self.driver.ocr_command({})
    -        print(ocr)
    -        self.switch.switch_to_ocr()
    -        self.click(xpath='//words/item[text() = "Flyme"]')
    -
    -
    -if __name__ == '__main__':
    -    capabilities = {
    -        "automationName": "UiAutomator2",
    -        "platformName": "Android",
    -        "appPackage": "com.meizu.flyme.flymebbs",
    -        "appActivity": "com.meizu.myplus.ui.splash.SplashActivity",
    -        "noReset": True,
    -    }
    -    options = UiAutomator2Options().load_capabilities(capabilities)
    -    seldom.main(app_server="http://127.0.0.1:4723", app_info=options, extensions=[OCRCommand])
    -

    根据上面代码示例,打印ocr变量得到一个JSON结构体。

    {
    -  "words": [
    -    {
    -      "text": "mEngine", "confidence": 88.47775268554688,
    -      "bbox": {"x0": 86, "y0": 509, "x1": 308, "y1": 560}
    -    },
    -    {
    -      "text": "Flyme", "confidence": 91.3454818725586,
    -       "bbox": {"x0": 316, "y0": 1132, "x1": 420, "y1": 1172}
    -      },
    -    {
    -      "text": "A9", "confidence": 34.86248779296875,
    -      "bbox": {"x0": 1017, "y0": 2565, "x1": 1078, "y1": 2595}
    -    }
    -  ],
    -  "lines": [
    -    {
    -      "text": "mEngine BY Ni0vEh 1 Bl\\n\\n", "confidence": 21.003677368164062,
    -     "bbox": {"x0": 86, "y0": 500, "x1": 674, "y1": 560}
    -    },
    -    {
    -      "text": "Flyme\\n\\n", "confidence": 91.3454818725586,
    -     "bbox": {"x0": 316, "y0": 1132, "x1": 420, "y1": 1172}
    -    },
    -    {
    -      "text": "A9\\n", "confidence": 34.86248779296875,
    -      "bbox": {"x0": 1017, "y0": 2565, "x1": 1078, "y1": 2595}
    -    }
    -  ],
    -  "blocks": [
    -    {
    -      "text": "mEngine BY Ni0vEh 1 Bl\\n\\n", "confidence": 21.003677368164062,
    -       "bbox": {"x0": 86, "y0": 500, "x1": 674, "y1": 560}
    -      },
    -    {
    -      "text": "Flyme\\n\\n", "confidence": 91.3454818725586,
    -      "bbox": {"x0": 316, "y0": 1132, "x1": 420, "y1": 1172}
    -    },
    -    {
    -      "text": "A9\\n", "confidence": 34.86248779296875, 
    -      "bbox": {"x0": 1017, "y0": 2565, "x1": 1078, "y1": 2595}
    -    }
    -  ]
    -}
    -

    JSON结构体说明:

    • wrods - Tesseract识别的单个单词的列表。

    • lines - Tesseract识别的文本行的列表。

    • blocks - Tesseract识别连续文本块的列表。

    每项都引用一个OCR对象,它们本身包含3个数据:

    • text:识别的文本。
    • confidence:Tesseract对于给定文本的OCR处理结果的置信度(范围在0到100之间)。
    • bbox:发现文本的边界框,边界框标记为x0、x1、y0和y1的值的对象。分别表文本的上下左右坐标位置,其中。这里,x0表示发现文本的左边x坐标,x1表示右边x坐标,y0表示上部y坐标,y1表示下部y坐标。
    `,30),e=[o];function c(l,u){return s(),a("div",null,e)}const r=n(t,[["render",c],["__file","extensions.html.vue"]]);export{r as default}; diff --git a/assets/index.html-29be9b02.js b/assets/index.html-29be9b02.js deleted file mode 100644 index 95096ee..0000000 --- a/assets/index.html-29be9b02.js +++ /dev/null @@ -1 +0,0 @@ -const e=JSON.parse('{"key":"v-8daa1a0e","path":"/","title":"","lang":"en-US","frontmatter":{"home":true,"heroText":"Seldom","heroImage":"/logo.jpeg","actions":[{"text":"快速上手→","link":"/getting-started/quick_start/","type":"primary"},{"text":"项目简介","link":"/introduce/","type":"secondary"}],"features":[{"title":"web/app/api 测试","details":"seldom是一个全功能测试框架。"},{"title":"快速","details":"提供脚手架快速创建自动化项目。"},{"title":"报告","details":"集成XTestRunner测试报告,现代美观。"},{"title":"断言","details":"提供丰富的断言,方便验证测试结果。"},{"title":"数据驱动","details":"支持Excel/CSV/JSON/YAML数据文件。"},{"title":"平台化","details":"seldom提供了平台化支持。"}],"footer":"MIT Licensed | Copyright © 2024-重定向科技"},"headers":[],"git":{"updatedTime":1717002654000,"contributors":[{"name":"defnngj","email":"defnngj@gmail.com","commits":4},{"name":"Yongchin","email":"yongchin39@qq.com","commits":3}]},"filePathRelative":"README.md"}');export{e as data}; diff --git a/assets/index.html-7f9a87de.js b/assets/index.html-7f9a87de.js deleted file mode 100644 index b18a73e..0000000 --- a/assets/index.html-7f9a87de.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as e,o as c,c as t}from"./app-72107ff1.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/index.html-76732133.js b/assets/index.html-8222f902.js similarity index 63% rename from assets/index.html-76732133.js rename to assets/index.html-8222f902.js index fb6ecf0..f892e93 100644 --- a/assets/index.html-76732133.js +++ b/assets/index.html-8222f902.js @@ -1 +1 @@ -import{_ as e,o as c,c as t}from"./app-a06a2d51.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; +import{_ as e,o as c,c as t}from"./app-9fb6f1b5.js";const n={};function _(o,r){return c(),t("div")}const a=e(n,[["render",_],["__file","index.html.vue"]]);export{a as default}; diff --git a/assets/installation.html-ce2cfb11.js b/assets/installation.html-ce2cfb11.js deleted file mode 100644 index ae2775b..0000000 --- a/assets/installation.html-ce2cfb11.js +++ /dev/null @@ -1,27 +0,0 @@ -import{_ as s,o as n,c as e,e as a}from"./app-a06a2d51.js";const l={},i=a(`

    Installation

    seldom的安装非常简单。

    • 快速安装

    目前已经上传 pypi.org ,可以使用pip命令安装。

    > pip install seldom
    -
    • 体验最新代码

    如果你想随时体验最新的代码,可以使用下面的命令。

    > pip install -U git+https://github.com/defnngj/seldom.git@master
    -
    • 安装依赖

    随着seldom 加入更多的功能,seldom不得不依赖其他的开源库。你可以在 requirements.txt 文件里面看到这些依赖。

    Appium-Python-Client>=4.1.0
    -XTestRunner>=1.7.2
    -loguru>=0.7.0
    -openpyxl>=3.0.3
    -pyyaml>=6.0
    -jsonschema>=4.10.0
    -jmespath>=0.10.0
    -pymysql>=1.0.0
    -genson==1.2.2
    -click~=8.1.3
    -python-dateutil==2.8.2
    -

    先通过 pip 命令安装这些依赖库,可以加快seldom的安装。

    > pip install -r requirements.txt
    -
    • 检查安装

    最后,我们可以通过pip show seldom命令检查安装。

    > pip show seldom
    -
    -Name: seldom
    -Version: 3.x.x
    -Summary: Seldom automation testing framework based on unittest.
    -Home-page: https://seldomqa.github.io
    -Author: bugmaster
    -Author-email: fnngj@126.com
    -License: Apache-2.0
    -Location: C:\\Python311\\Lib\\site-packages
    -Requires:  Appium-Python-Client, click, genson, jmespath, jsonschema, loguru, openpyxl, pymysql, python-dateutil, pyyaml, requests, websocket-client, XTestRunner
    -Required-by:
    -
    `,16),t=[i];function p(o,r){return n(),e("div",null,t)}const c=s(l,[["render",p],["__file","installation.html.vue"]]);export{c as default}; diff --git a/assets/installation.html-9ca7cf6d.js b/assets/installation.html-eed58102.js similarity index 98% rename from assets/installation.html-9ca7cf6d.js rename to assets/installation.html-eed58102.js index 5a4fcf6..2c502d9 100644 --- a/assets/installation.html-9ca7cf6d.js +++ b/assets/installation.html-eed58102.js @@ -1,4 +1,4 @@ -import{_ as s,o as n,c as e,e as a}from"./app-72107ff1.js";const l={},i=a(`

    Installation

    seldom的安装非常简单。

    • 快速安装

    目前已经上传 pypi.org ,可以使用pip命令安装。

    > pip install seldom
    +import{_ as s,o as n,c as e,e as a}from"./app-9fb6f1b5.js";const l={},i=a(`

    Installation

    seldom的安装非常简单。

    • 快速安装

    目前已经上传 pypi.org ,可以使用pip命令安装。

    > pip install seldom
     
    • 体验最新代码

    如果你想随时体验最新的代码,可以使用下面的命令。

    > pip install -U git+https://github.com/defnngj/seldom.git@master
     
    • 安装依赖

    随着seldom 加入更多的功能,seldom不得不依赖其他的开源库。你可以在 requirements.txt 文件里面看到这些依赖。

    Appium-Python-Client>=4.1.0
     XTestRunner>=1.7.2
    diff --git a/assets/introduce.html-27cd219e.js b/assets/introduce.html-27cd219e.js
    new file mode 100644
    index 0000000..8cb5295
    --- /dev/null
    +++ b/assets/introduce.html-27cd219e.js
    @@ -0,0 +1 @@
    +import{_ as l,r as s,o as a,c as i,a as t,b as d,d as o,e as r}from"./app-9fb6f1b5.js";const p="/image/book.jpg",c={},n=r('

    介绍

    新书推荐

    京东链接

    ',3),h={href:"https://item.jd.com/14859108.html",target:"_blank",rel:"noopener noreferrer"},m={href:"https://detail.tmall.com/item.htm?id=852715481274&skuId=5817727406269",target:"_blank",rel:"noopener noreferrer"},u={href:"https://product.dangdang.com/29809610.html",target:"_blank",rel:"noopener noreferrer"},y=r('

    依托于 SeldomQA 相关项目的开发和维护,在 自动化测试框架设计定制化测试报告设计设计模式,以及测试平台开发 方面有着深厚技术积累和独特的设计理念。

    一本真正介绍 自动化测试框架设计 的书终于出版了,书中浅显易懂的介绍了 SeldomQA 相关项目中的诸多设计和封装技术。并且,介绍了一个开源自动化测试框架从设计到发布的整个流程。 如果你正在使用SeldomQA相关项目之余,想了解他们背后的设计,那么这本书非常值得购买。

    seldom框架

    特点

    seldom 是基于 unittest 的全功能自动化测试框架;针对自动化测试达到开箱即用。

    seldom特点

    • 支持测试类型(web/app/api)
    • 丰富的断言
    • 生成随机测试数据
    • 用例依赖
    • 用例分类标签
    • 支持发送(邮件、钉钉、飞书、企微)消息等
    • 日志打印
    • 缓存cache
    • 命令行工具
    • 强大的数据驱动(JSON/YAML/CSV/EXCEL)
    • HTML/XML报告
    • 失败重跑&截图
    • 数据库操作(MySQL/sqlite3/Mongodb)
    • 支持平台化

    设计理念

    简单一句话就是回到最初写代码的样子。

    自动化测试框架很多,只有在测试领域有一个比较奇怪的现象,如何用不写代码的方式解决自动化问题。为此,我们发明了用特定领域语言写用例,发明了用 excel 写用例,发明了用 YAML/JSON 写用例。这些方案看似简化了用例的编写,但是,会让解决复杂的问题变得更复杂。比如实现个分支判断/循环,传递参数,调用封装的步骤,编程语言中用 if/for 、变量、函数就实现了,但是用非编程语言的方式写用例处理起来就很麻烦。最终,并不能完全脱离编程,那么为什么不一开始就选择一个编程框架呢?

    然而,seldom的定位是尽量用简单的设计去解决复杂问题,例如 Flask、requests、yagmail...等,这些框架/库都有一个共同的特点,用简单的方式去解决复杂的问题,在编程语言这个层面,并不会给你太多限制,你可以完全使用它,也可以只用一部分,也可以平滑的实现它不支持的功能。

    seldom的目标以就让你用最少的代码编写自动化测试用例,当遇到seldom没有的功能,你可以方便的进行扩展。-- 这就是seldom的设计理念。

    发展历史

    2015年7月15号我在github上提交一个自动化项目,命名为:pyse, 即各取了pythonselenium前两个字符。项目非常简单核心就三个文件。

    • pyse.py:针对 selenium API做了简单封装。
    • HTMLTestRunner.py: 修改的HTMLTestRunner报告。
    • TestRunner.py: 一个简单的 unittest运行器。

    之后项目断断续续的在维护,直到2019年,也许是太闲了,加上对UI自动化有了更深入的理解,重新投入主要精力维护pyse项目。

    后来就需要将提交到pypi,这样更方便通过pip安装,发现 pyse 早已经被占用了,后来更名为seldom ,其实命名没有太多寓意,就是看他长得和selenium比较接近。

    2020年1月发布1.0版本,之所以发布1.0 是因为自认为框架的功能比较成熟了,并且花费时间补充了文档。大家都不重视文档,其实文档非常重要,也需要花大量的时间更新和维护。有时候你加个功能很简单,编写说明文档和使用示例就要花费等同的时间。

    1.0 版本之后,项目核心围绕着 selenium API的封装 和 unittest框架扩展(seldom基于unittest)等。

    2021年4月正式发布 2.0,集成requests, 正式支持http接口测试。起因是发现cypress支持http调用,哦,原来UI测试工具也可以去做接口,格局一下子打开了!如何在不影响现有selenium API的情况下集成requests是2.0考虑的重点。

    2022年1月seldom项目正式在公司内部推广使用,当时我们做了几版的接口测试平台,平台的开发维护成本比较高,对于复杂的场景用例,编写成本比框架还要复杂简单;功能也依赖于平台所提供的,相比较而言,框架却有最大的灵活性,可以很好的基于业务做各种设计和封装。

    因为在公司得到推广使用,seldom明显进入了更加快速的迭代开发阶段,并且稳定性、可用性灰得到了很大的提升。

    seldom 3.0 背景 seldom集成App测试是顺理成章的事情,早在几个月前我已经在公司项目中尝试 seldom + appium 进行App自动化测试。App自动化的维护成本确实比接口要高许多,这是由App本身的特点决定的,框架很难做到实质上的改变。

    2022年10月seldom 3.0 beta发布,之所以选择appium有几个原因:

    • appium 是由商业工具在维护,历史比较长,不会随意停止维护。
    • appium 应用更加广泛,使用得人更多,支持得平台多(android/ios/flutter)
    • appium 继承selenium,对于seldom来说对原有API改动最小。

    目前,seldom 3.0 正式版已经发布,欢迎使用。

    seldom vs pytest

    seldom 是建立在 unittest 的基础上的自动化测试框架。与 pytest进行对比,无疑相当于像拿一台电脑与一颗 intel CPU 进行比较,虽然 intel CPU 很强大,但我们无法直接拿一个CPU打游戏,对吧? pytest 就像一个 CPU ,虽然很强大,但无法直接拿来做自动化测试,比如配合各种测试库。而seldom不需要额外安装测试库,即可开始编写自动化测试用例。

    • seldom vs pytest 对比差异
    功能seldompytest
    web UI测试支持 ✅支持(需安装 selenium) ⚠️
    web UI断言支持(assertText、assertTitle、assertElement) ✅不支持 ❌
    playwright支持(需安装playwright) ⚠️支持(playwright提供playwright-pytest插件) ✅
    失败截图支持(自动实现) ✅支持(需要设置) ✅
    http接口测试支持 ✅支持(需安装 requests) ⚠️
    http接口断言支持(assertJSON、assertPath、assertSchema) ✅不支持 ❌
    app UI测试支持 ✅支持(需安装 appium) ⚠️
    Page Object模式支持(推荐poium) ✅支持(推荐poium) ✅
    脚手架支持(快速创建项目) ✅不支持 ❌
    生成随机测试数据支持testdata不支持 ❌
    发送消息支持(email、钉钉、飞书、微信)✅不支持 ❌
    log日志支持 ✅不支持 ❌
    数据库操作支持(sqlite3、MySQL、SQL Server) ✅不支持 ❌
    用例依赖支持@depend()@pytest.mark.dependency()支持 ✅
    失败重跑支持rerunpytest-rerunfailures 支持 ✅
    用例分类标签支持@label()@pytest.mark.xxx支持 ✅
    HTML测试报告支持 ✅pytest-html、allure ✅
    XML测试报告支持 ✅自带 --junit-xml
    数据驱动方法@data()@pytest.mark.parametrize()
    数据驱动文件@file_data()(JSON\\YAML\\CSV\\Excel) ✅不支持 ❌
    钩子函数confrun.py用例运行钩子 ⚠️conftest.py 功能更强大 ✅
    命令行工具CLI支持seldom支持pytest
    并发执行不支持 ❌pytest-xdist、pytest-parallel ✅
    平台化支持(seldom-platform)✅不支持 ❌
    第三方插件seldom(unittest)的生态比较糟糕 ⚠️pytest有丰富插件生态 ✅

    说明

    • ✅ : 表示支持。

    • ⚠️: 支持,但支持的不好,或没有对方好。

    • ❌ : 不支持,表示框架没有该功能,第三方插件也没有。

    ',32);function _(f,b){const e=s("ExternalLinkIcon");return a(),i("div",null,[n,t("p",null,[d("京东 "),t("a",h,[d("购买链接"),o(e)])]),t("p",null,[d("天猫 "),t("a",m,[d("购买链接"),o(e)])]),t("p",null,[d("当当 "),t("a",u,[d("购买链接"),o(e)])]),y])}const x=l(c,[["render",_],["__file","introduce.html.vue"]]);export{x as default}; diff --git a/assets/introduce.html-6487c4a7.js b/assets/introduce.html-2d1ffccb.js similarity index 85% rename from assets/introduce.html-6487c4a7.js rename to assets/introduce.html-2d1ffccb.js index 2dc0046..7ae3781 100644 --- a/assets/introduce.html-6487c4a7.js +++ b/assets/introduce.html-2d1ffccb.js @@ -1 +1 @@ -const e=JSON.parse('{"key":"v-d08d435a","path":"/introduce.html","title":"介绍","lang":"en-US","frontmatter":{},"headers":[{"level":2,"title":"新书推荐","slug":"新书推荐","link":"#新书推荐","children":[]},{"level":2,"title":"seldom框架","slug":"seldom框架","link":"#seldom框架","children":[{"level":3,"title":"特点","slug":"特点","link":"#特点","children":[]},{"level":3,"title":"设计理念","slug":"设计理念","link":"#设计理念","children":[]},{"level":3,"title":"发展历史","slug":"发展历史","link":"#发展历史","children":[]},{"level":3,"title":"seldom vs pytest","slug":"seldom-vs-pytest","link":"#seldom-vs-pytest","children":[]}]}],"git":{"updatedTime":1732010155000,"contributors":[{"name":"defnngj","email":"defnngj@gmail.com","commits":12},{"name":"yangqing","email":"yongchin39@qq.com","commits":1}]},"filePathRelative":"introduce.md"}');export{e as data}; +const e=JSON.parse('{"key":"v-d08d435a","path":"/introduce.html","title":"介绍","lang":"en-US","frontmatter":{},"headers":[{"level":2,"title":"新书推荐","slug":"新书推荐","link":"#新书推荐","children":[]},{"level":2,"title":"seldom框架","slug":"seldom框架","link":"#seldom框架","children":[{"level":3,"title":"特点","slug":"特点","link":"#特点","children":[]},{"level":3,"title":"设计理念","slug":"设计理念","link":"#设计理念","children":[]},{"level":3,"title":"发展历史","slug":"发展历史","link":"#发展历史","children":[]},{"level":3,"title":"seldom vs pytest","slug":"seldom-vs-pytest","link":"#seldom-vs-pytest","children":[]}]}],"git":{"updatedTime":1734076727000,"contributors":[{"name":"defnngj","email":"defnngj@gmail.com","commits":13},{"name":"yangqing","email":"yongchin39@qq.com","commits":1}]},"filePathRelative":"introduce.md"}');export{e as data}; diff --git a/assets/introduce.html-56f089a5.js b/assets/introduce.html-56f089a5.js deleted file mode 100644 index f76c91c..0000000 --- a/assets/introduce.html-56f089a5.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as l,r as s,o as a,c as i,a as t,b as d,d as o,e as r}from"./app-a06a2d51.js";const p="/image/book.jpg",c={},n=r('

    介绍

    新书推荐

    京东链接

    ',3),h={href:"https://item.jd.com/10124939676219.html",target:"_blank",rel:"noopener noreferrer"},m={href:"https://detail.tmall.com/item.htm?id=852715481274&skuId=5817727406269",target:"_blank",rel:"noopener noreferrer"},u={href:"https://product.dangdang.com/29809610.html",target:"_blank",rel:"noopener noreferrer"},y=r('

    依托于 SeldomQA 相关项目的开发和维护,在 自动化测试框架设计定制化测试报告设计设计模式,以及测试平台开发 方面有着深厚技术积累和独特的设计理念。

    一本真正介绍 自动化测试框架设计 的书终于出版了,书中浅显易懂的介绍了 SeldomQA 相关项目中的诸多设计和封装技术。并且,介绍了一个开源自动化测试框架从设计到发布的整个流程。 如果你正在使用SeldomQA相关项目之余,想了解他们背后的设计,那么这本书非常值得购买。

    seldom框架

    特点

    seldom 是基于 unittest 的全功能自动化测试框架;针对自动化测试达到开箱即用。

    seldom特点

    • 支持测试类型(web/app/api)
    • 丰富的断言
    • 生成随机测试数据
    • 用例依赖
    • 用例分类标签
    • 支持发送(邮件、钉钉、飞书、企微)消息等
    • 日志打印
    • 缓存cache
    • 命令行工具
    • 强大的数据驱动(JSON/YAML/CSV/EXCEL)
    • HTML/XML报告
    • 失败重跑&截图
    • 数据库操作(MySQL/sqlite3/Mongodb)
    • 支持平台化

    设计理念

    简单一句话就是回到最初写代码的样子。

    自动化测试框架很多,只有在测试领域有一个比较奇怪的现象,如何用不写代码的方式解决自动化问题。为此,我们发明了用特定领域语言写用例,发明了用 excel 写用例,发明了用 YAML/JSON 写用例。这些方案看似简化了用例的编写,但是,会让解决复杂的问题变得更复杂。比如实现个分支判断/循环,传递参数,调用封装的步骤,编程语言中用 if/for 、变量、函数就实现了,但是用非编程语言的方式写用例处理起来就很麻烦。最终,并不能完全脱离编程,那么为什么不一开始就选择一个编程框架呢?

    然而,seldom的定位是尽量用简单的设计去解决复杂问题,例如 Flask、requests、yagmail...等,这些框架/库都有一个共同的特点,用简单的方式去解决复杂的问题,在编程语言这个层面,并不会给你太多限制,你可以完全使用它,也可以只用一部分,也可以平滑的实现它不支持的功能。

    seldom的目标以就让你用最少的代码编写自动化测试用例,当遇到seldom没有的功能,你可以方便的进行扩展。-- 这就是seldom的设计理念。

    发展历史

    2015年7月15号我在github上提交一个自动化项目,命名为:pyse, 即各取了pythonselenium前两个字符。项目非常简单核心就三个文件。

    • pyse.py:针对 selenium API做了简单封装。
    • HTMLTestRunner.py: 修改的HTMLTestRunner报告。
    • TestRunner.py: 一个简单的 unittest运行器。

    之后项目断断续续的在维护,直到2019年,也许是太闲了,加上对UI自动化有了更深入的理解,重新投入主要精力维护pyse项目。

    后来就需要将提交到pypi,这样更方便通过pip安装,发现 pyse 早已经被占用了,后来更名为seldom ,其实命名没有太多寓意,就是看他长得和selenium比较接近。

    2020年1月发布1.0版本,之所以发布1.0 是因为自认为框架的功能比较成熟了,并且花费时间补充了文档。大家都不重视文档,其实文档非常重要,也需要花大量的时间更新和维护。有时候你加个功能很简单,编写说明文档和使用示例就要花费等同的时间。

    1.0 版本之后,项目核心围绕着 selenium API的封装 和 unittest框架扩展(seldom基于unittest)等。

    2021年4月正式发布 2.0,集成requests, 正式支持http接口测试。起因是发现cypress支持http调用,哦,原来UI测试工具也可以去做接口,格局一下子打开了!如何在不影响现有selenium API的情况下集成requests是2.0考虑的重点。

    2022年1月seldom项目正式在公司内部推广使用,当时我们做了几版的接口测试平台,平台的开发维护成本比较高,对于复杂的场景用例,编写成本比框架还要复杂简单;功能也依赖于平台所提供的,相比较而言,框架却有最大的灵活性,可以很好的基于业务做各种设计和封装。

    因为在公司得到推广使用,seldom明显进入了更加快速的迭代开发阶段,并且稳定性、可用性灰得到了很大的提升。

    seldom 3.0 背景 seldom集成App测试是顺理成章的事情,早在几个月前我已经在公司项目中尝试 seldom + appium 进行App自动化测试。App自动化的维护成本确实比接口要高许多,这是由App本身的特点决定的,框架很难做到实质上的改变。

    2022年10月seldom 3.0 beta发布,之所以选择appium有几个原因:

    • appium 是由商业工具在维护,历史比较长,不会随意停止维护。
    • appium 应用更加广泛,使用得人更多,支持得平台多(android/ios/flutter)
    • appium 继承selenium,对于seldom来说对原有API改动最小。

    目前,seldom 3.0 正式版已经发布,欢迎使用。

    seldom vs pytest

    seldom 是建立在 unittest 的基础上的自动化测试框架。与 pytest进行对比,无疑相当于像拿一台电脑与一颗 intel CPU 进行比较,虽然 intel CPU 很强大,但我们无法直接拿一个CPU打游戏,对吧? pytest 就像一个 CPU ,虽然很强大,但无法直接拿来做自动化测试,比如配合各种测试库。而seldom不需要额外安装测试库,即可开始编写自动化测试用例。

    • seldom vs pytest 对比差异
    功能seldompytest
    web UI测试支持 ✅支持(需安装 selenium) ⚠️
    web UI断言支持(assertText、assertTitle、assertElement) ✅不支持 ❌
    playwright支持(需安装playwright) ⚠️支持(playwright提供playwright-pytest插件) ✅
    失败截图支持(自动实现) ✅支持(需要设置) ✅
    http接口测试支持 ✅支持(需安装 requests) ⚠️
    http接口断言支持(assertJSON、assertPath、assertSchema) ✅不支持 ❌
    app UI测试支持 ✅支持(需安装 appium) ⚠️
    Page Object模式支持(推荐poium) ✅支持(推荐poium) ✅
    脚手架支持(快速创建项目) ✅不支持 ❌
    生成随机测试数据支持testdata不支持 ❌
    发送消息支持(email、钉钉、飞书、微信)✅不支持 ❌
    log日志支持 ✅不支持 ❌
    数据库操作支持(sqlite3、MySQL、SQL Server) ✅不支持 ❌
    用例依赖支持@depend()@pytest.mark.dependency()支持 ✅
    失败重跑支持rerunpytest-rerunfailures 支持 ✅
    用例分类标签支持@label()@pytest.mark.xxx支持 ✅
    HTML测试报告支持 ✅pytest-html、allure ✅
    XML测试报告支持 ✅自带 --junit-xml
    数据驱动方法@data()@pytest.mark.parametrize()
    数据驱动文件@file_data()(JSON\\YAML\\CSV\\Excel) ✅不支持 ❌
    钩子函数confrun.py用例运行钩子 ⚠️conftest.py 功能更强大 ✅
    命令行工具CLI支持seldom支持pytest
    并发执行不支持 ❌pytest-xdist、pytest-parallel ✅
    平台化支持(seldom-platform)✅不支持 ❌
    第三方插件seldom(unittest)的生态比较糟糕 ⚠️pytest有丰富插件生态 ✅

    说明

    • ✅ : 表示支持。

    • ⚠️: 支持,但支持的不好,或没有对方好。

    • ❌ : 不支持,表示框架没有该功能,第三方插件也没有。

    ',32);function _(f,b){const e=s("ExternalLinkIcon");return a(),i("div",null,[n,t("p",null,[d("京东 "),t("a",h,[d("购买链接"),o(e)])]),t("p",null,[d("天猫 "),t("a",m,[d("购买链接"),o(e)])]),t("p",null,[d("当当 "),t("a",u,[d("购买链接"),o(e)])]),y])}const x=l(c,[["render",_],["__file","introduce.html.vue"]]);export{x as default}; diff --git a/assets/introduce.html-93fae745.js b/assets/introduce.html-93fae745.js deleted file mode 100644 index dbbcf37..0000000 --- a/assets/introduce.html-93fae745.js +++ /dev/null @@ -1 +0,0 @@ -const e=JSON.parse('{"key":"v-d08d435a","path":"/introduce.html","title":"介绍","lang":"en-US","frontmatter":{},"headers":[{"level":2,"title":"seldom 介绍","slug":"seldom-介绍","link":"#seldom-介绍","children":[]},{"level":2,"title":"seldom 理念","slug":"seldom-理念","link":"#seldom-理念","children":[]},{"level":2,"title":"seldom 历史","slug":"seldom-历史","link":"#seldom-历史","children":[]},{"level":2,"title":"seldom vs pytest","slug":"seldom-vs-pytest","link":"#seldom-vs-pytest","children":[]}],"git":{"updatedTime":1687279316000,"contributors":[{"name":"defnngj","email":"defnngj@gmail.com","commits":11},{"name":"yangqing","email":"yongchin39@qq.com","commits":1}]},"filePathRelative":"introduce.md"}');export{e as data}; diff --git a/assets/introduce.html-a72107fb.js b/assets/introduce.html-a72107fb.js deleted file mode 100644 index d43a915..0000000 --- a/assets/introduce.html-a72107fb.js +++ /dev/null @@ -1 +0,0 @@ -import{_ as t,o as d,c as e,e as l}from"./app-72107ff1.js";const o={},s=l('

    介绍

    seldom 介绍

    seldom 是基于 unittest 的全功能自动化测试框架;针对自动化测试达到开箱即用。

    seldom特点

    • 支持测试类型(web/app/api)
    • 丰富的断言
    • 生成随机测试数据
    • 用例依赖
    • 用例分类标签
    • 支持发送(邮件、钉钉、飞书、企微)消息等
    • 日志打印
    • 缓存cache
    • 命令行工具
    • 强大的数据驱动(JSON/YAML/CSV/EXCEL)
    • HTML/XML报告
    • 失败重跑&截图
    • 数据库操作(MySQL/sqlite3/Mongodb)
    • 支持平台化

    seldom 理念

    简单一句话就是回到最初写代码的样子。

    自动化测试框架很多,只有在测试领域有一个比较奇怪的现象,如何用不写代码的方式解决自动化问题。为此,我们发明了用特定领域语言写用例,发明了用 excel 写用例,发明了用 YAML/JSON 写用例。这些方案看似简化了用例的编写,但是,会让解决复杂的问题变得更复杂。比如实现个分支判断/循环,传递参数,调用封装的步骤,编程语言中用 if/for 、变量、函数就实现了,但是用非编程语言的方式写用例处理起来就很麻烦。最终,并不能完全脱离编程,那么为什么不一开始就选择一个编程框架呢?

    然而,seldom的定位是尽量用简单的设计去解决复杂问题,例如 Flask、requests、yagmail...等,这些框架/库都有一个共同的特点,用简单的方式去解决复杂的问题,在编程语言这个层面,并不会给你太多限制,你可以完全使用它,也可以只用一部分,也可以平滑的实现它不支持的功能。

    seldom的目标以就让你用最少的代码编写自动化测试用例,当遇到seldom没有的功能,你可以方便的进行扩展。-- 这就是seldom的设计理念。

    seldom 历史

    2015年7月15号我在github上提交一个自动化项目,命名为:pyse, 即各取了pythonselenium前两个字符。项目非常简单核心就三个文件。

    • pyse.py:针对 selenium API做了简单封装。
    • HTMLTestRunner.py: 修改的HTMLTestRunner报告。
    • TestRunner.py: 一个简单的 unittest运行器。

    之后项目断断续续的在维护,直到2019年,也许是太闲了,加上对UI自动化有了更深入的理解,重新投入主要精力维护pyse项目。

    后来就需要将提交到pypi,这样更方便通过pip安装,发现 pyse 早已经被占用了,后来更名为seldom,其实命名没有太多寓意,就是看他长得和selenium比较接近。

    2020年1月发布1.0版本,之所以发布1.0 是因为自认为框架的功能比较成熟了,并且花费时间补充了文档。大家都不重视文档,其实文档非常重要,也需要花大量的时间更新和维护。有时候你加个功能很简单,编写说明文档和使用示例就要花费等同的时间。

    1.0 版本之后,项目核心围绕着 selenium API的封装 和 unittest框架扩展(seldom基于unittest)等。

    2021年4月正式发布 2.0,集成requests, 正式支持http接口测试。起因是发现cypress支持http调用,哦,原来UI测试工具也可以去做接口,格局一下子打开了!如何在不影响现有selenium API的情况下集成requests是2.0考虑的重点。

    2022年1月seldom项目正式在公司内部推广使用,当时我们做了几版的接口测试平台,平台的开发维护成本比较高,对于复杂的场景用例,编写成本比框架还要复杂简单;功能也依赖于平台所提供的,相比较而言,框架却有最大的灵活性,可以很好的基于业务做各种设计和封装。

    因为在公司得到推广使用,seldom明显进入了更加快速的迭代开发阶段,并且稳定性、可用性灰得到了很大的提升。

    seldom 3.0 背景 seldom集成App测试是顺理成章的事情,早在几个月前我已经在公司项目中尝试 seldom + appium 进行App自动化测试。App自动化的维护成本确实比接口要高许多,这是由App本身的特点决定的,框架很难做到实质上的改变。

    2022年10月seldom 3.0 beta发布,之所以选择appium有几个原因:

    • appium 是由商业工具在维护,历史比较长,不会随意停止维护。
    • appium 应用更加广泛,使用得人更多,支持得平台多(android/ios/flutter)
    • appium 继承selenium,对于seldom来说对原有API改动最小。

    目前,seldom 3.0 正式版已经发布,欢迎使用。

    seldom vs pytest

    seldom 是建立在 unittest 的基础上的自动化测试框架。与 pytest进行对比,无疑相当于像拿一台电脑与一颗 intel CPU 进行比较,虽然 intel CPU 很强大,但我们无法直接拿一个CPU打游戏,对吧? pytest 就像一个 CPU,虽然很强大,但无法直接拿来做自动化测试,比如配合各种测试库。而seldom不需要额外安装测试库,即可开始编写自动化测试用例。

    • seldom vs pytest 对比差异
    功能seldompytest
    web UI测试支持 ✅支持(需安装 selenium) ⚠️
    web UI断言支持(assertText、assertTitle、assertElement) ✅不支持 ❌
    playwright支持(需安装playwright) ⚠️支持(playwright提供playwright-pytest插件) ✅
    失败截图支持(自动实现) ✅支持(需要设置) ✅
    http接口测试支持 ✅支持(需安装 requests) ⚠️
    http接口断言支持(assertJSON、assertPath、assertSchema) ✅不支持 ❌
    app UI测试支持 ✅支持(需安装 appium) ⚠️
    Page Object模式支持(推荐poium) ✅支持(推荐poium) ✅
    脚手架支持(快速创建项目) ✅不支持 ❌
    生成随机测试数据支持testdata不支持 ❌
    发送消息支持(email、钉钉、飞书、微信)✅不支持 ❌
    log日志支持 ✅不支持 ❌
    数据库操作支持(sqlite3、MySQL、SQL Server) ✅不支持 ❌
    用例依赖支持@depend()@pytest.mark.dependency()支持 ✅
    失败重跑支持rerunpytest-rerunfailures 支持 ✅
    用例分类标签支持@label()@pytest.mark.xxx支持 ✅
    HTML测试报告支持 ✅pytest-html、allure ✅
    XML测试报告支持 ✅自带 --junit-xml
    数据驱动方法@data()@pytest.mark.parametrize()
    数据驱动文件@file_data()(JSON\\YAML\\CSV\\Excel) ✅不支持 ❌
    钩子函数confrun.py用例运行钩子 ⚠️conftest.py 功能更强大 ✅
    命令行工具CLI支持seldom支持pytest
    并发执行不支持 ❌pytest-xdist、pytest-parallel ✅
    平台化支持(seldom-platform)✅不支持 ❌
    第三方插件seldom(unittest)的生态比较糟糕 ⚠️pytest有丰富插件生态 ✅

    说明

    • ✅ : 表示支持。

    • ⚠️: 支持,但支持的不好,或没有对方好。

    • ❌ : 不支持,表示框架没有该功能,第三方插件也没有。

    ',30),r=[s];function i(p,a){return d(),e("div",null,r)}const m=t(o,[["render",i],["__file","introduce.html.vue"]]);export{m as default}; diff --git a/assets/more.html-65684271.js b/assets/more.html-5e1828bb.js similarity index 90% rename from assets/more.html-65684271.js rename to assets/more.html-5e1828bb.js index 8627ede..86b634a 100644 --- a/assets/more.html-65684271.js +++ b/assets/more.html-5e1828bb.js @@ -1,4 +1,4 @@ -import{_ as p,r as e,o,c,a as n,b as s,d as l,e as a}from"./app-a06a2d51.js";const i="/image/fiddler.png",u="/image/fiddler2.png",r={},k=a('

    更多功能

    har to case

    对于不熟悉 Requests 库的人来说,通过Seldom来写接口测试用例还是会有一点难度。于是,seldom 提供了har 文件转 case 的命令。

    首先,打开fiddler 工具进行抓包,选中某一个请求。

    然后,选择菜单栏:file -> Export Sessions -> Selected Sessions...

    选择导出的文件格式。

    点击next 保存为demo.har 文件。

    最后,通过seldom -h2c 转为demo.py 脚本文件。

    > seldom -h2c demo.har
    +import{_ as p,r as e,o,c,a as n,b as s,d as l,e as a}from"./app-9fb6f1b5.js";const i="/image/fiddler.png",u="/image/fiddler2.png",r={},k=a('

    更多功能

    har to case

    对于不熟悉 Requests 库的人来说,通过Seldom来写接口测试用例还是会有一点难度。于是,seldom 提供了har 文件转 case 的命令。

    首先,打开fiddler 工具进行抓包,选中某一个请求。

    然后,选择菜单栏:file -> Export Sessions -> Selected Sessions...

    选择导出的文件格式。

    点击next 保存为demo.har 文件。

    最后,通过seldom -h2c 转为demo.py 脚本文件。

    > seldom -h2c demo.har
     
     2021-06-14 18:05:50 [INFO] Start to generate testcase.
     2021-06-14 18:05:50 [INFO] created file: ...\\demo.py
    @@ -11,7 +11,10 @@ import{_ as p,r as e,o,c,a as n,b as s,d as l,e as a}from"./app-a06a2d51.js";con
             self.url = "http://httpbin.org/post"
     
         def test_case(self):
    -        headers = {"User-Agent": "python-requests/2.25.0", "Accept-Encoding": "gzip, deflate", "Accept": "application/json", "Connection": "keep-alive", "Host": "httpbin.org", "Content-Length": "36", "Origin": "http://httpbin.org", "Content-Type": "application/json", "Cookie": "lang=zh"}
    +        headers = {"User-Agent": "python-requests/2.25.0", "Accept-Encoding": "gzip, deflate",
    +                   "Accept": "application/json", "Connection": "keep-alive", "Host": "httpbin.org",
    +                   "Content-Length": "36", "Origin": "http://httpbin.org", "Content-Type": "application/json",
    +                   "Cookie": "lang=zh"}
             cookies = {"lang": "zh"}
             self.post(self.url, json={"key1": "value1", "key2": "value2"}, headers=headers, cookies=cookies)
             self.assertStatusCode(200)
    @@ -20,15 +23,15 @@ import{_ as p,r as e,o,c,a as n,b as s,d as l,e as a}from"./app-a06a2d51.js";con
     if __name__ == '__main__':
         seldom.main()
     
    -

    swagger to case

    seldom 3.6 版本支持。

    seldom 提供了swaggercase 的命令。 使用 seldom -s2c 命令。

    > seldom -s2c swagger.json
    +

    swagger to case

    seldom 3.6 版本支持。

    seldom 提供了swaggercase 的命令。 使用 seldom -s2c 命令。

    > seldom -s2c swagger.json
     
     2024-03-04 00:02:22 | INFO     | core.py | Start to generate testcase.
     2024-03-04 00:02:22 | INFO     | core.py | created file: ...\\swagger.py
     

    将swagger文档转为 seldom 自动化测试用例。

    import seldom
     
     
    -class TestRequest(seldom.TestCase): 
    -    
    +class TestRequest(seldom.TestCase):
    +
         def test_pet_petId_uploadImage_api_post(self):
             url = f"https://petstore.swagger.io/pet/{petId}/uploadImage"
             params = {}
    @@ -90,6 +93,7 @@ import{_ as p,r as e,o,c,a as n,b as s,d as l,e as a}from"./app-a06a2d51.js";con
     curl -X PUT  -H 'Content-Type: application/json' -H 'token: 123' -d '{"key": "value"}' http://httpbin.org/put
     

    接口数据依赖

    在场景测试中,我们需要利用上一个接口的数据,调用下一个接口。

    • 简单的接口依赖
    import seldom
     
    +
     class TestRespData(seldom.TestCase):
     
         def test_data_dependency(self):
    @@ -103,13 +107,13 @@ import{_ as p,r as e,o,c,a as n,b as s,d as l,e as a}from"./app-a06a2d51.js";con
             username = self.response["headers"]["X-Account-Fullname"]
             self.post("/post", data={'username': username})
             self.assertStatusCode(200)
    -

    seldom提供了self.response用于记录上个接口返回的结果,直接拿来用即可。

    • 封装接口依赖
    1. 创建公共模块
    # common.py
    -from seldom.request import check_response 
    +

    seldom提供了self.response用于记录上个接口返回的结果,直接拿来用即可。

    • 封装接口依赖
    1. 创建公共模块
    # common.py
    +from seldom.request import check_response
     from seldom.request import HttpRequest
     
     
     class Common(HttpRequest):
    -    
    +
         @check_response(
             describe="获取登录用户名",
             status_code=200,
    @@ -157,7 +161,7 @@ import{_ as p,r as e,o,c,a as n,b as s,d as l,e as a}from"./app-a06a2d51.js";con
     2023-02-14 23:51:49 request.py | DEBUG | Execute get_login_user - response:
      {'args': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Account': 'bugmaster', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.28.1', 'X-Amzn-Trace-Id': 'Root=1-63ebae14-1e629b132c21f68e23ffeb33'}, 'origin': '173.248.248.88', 'url': 'http://httpbin.org/get'}
     2023-02-14 23:51:49 request.py | INFO | Execute get_login_user - 获取登录用户名 success!
    -

    @check_response 专门用于处理封装的方法。

    参数说明:

    • describe : 封装方法描述。
    • status_code: 判断接口返回的 HTTP 状态码,默认200
    • ret: 提取接口返回的字段,参考jmespath 提取规则。
    • check: 检查接口返回的字段。参考jmespath 提取规则。
    • debug: 开启debug,打印更多信息。
    1. 引用公共模块
    import seldom
    +

    @check_response 专门用于处理封装的方法。

    参数说明:

    • describe: 封装方法描述。
    • status_code: 判断接口返回的 HTTP 状态码,默认200
    • ret: 提取接口返回的字段,参考jmespath 提取规则。
    • check: 检查接口返回的字段。参考jmespath 提取规则。
    • debug: 开启debug,打印更多信息。
    1. 引用公共模块
    import seldom
     from common import Common
     
     
    @@ -253,8 +257,10 @@ import{_ as p,r as e,o,c,a as n,b as s,d as l,e as a}from"./app-a06a2d51.js";con
             print(f"jsonpath3 --> {jsonpath3}")
             print(f"jsonpath4 --> {jsonpath4}")
             print(f"jsonpath5 --> {jsonpath5}")
    +
    +
     ...
    -

    说明:

    • response: 保存接口返回的数据,可以直接以,字典列表的方式提取。
    • jmespath(): 根据 JMESPath 语法规则,默认提取接口返回的数据,也可指定resposne数据提取。
    • jsonpath(): 根据 JsonPath 语法规则,默认提取接口返回的数据, index指定下标,也可指定resposne数据提取。

    运行结果:

    2022-05-19 00:57:08 log.py | DEBUG | [response]:
    +

    说明:

    • response: 保存接口返回的数据,可以直接以,字典列表的方式提取。
    • jmespath(): 根据 JMESPath 语法规则,默认提取接口返回的数据,也可指定resposne数据提取。
    • jsonpath(): 根据 JsonPath 语法规则,默认提取接口返回的数据, index指定下标,也可指定resposne数据提取。

    运行结果:

    2022-05-19 00:57:08 log.py | DEBUG | [response]:
      {'args': {'age': '18', 'hobby': ['basketball', 'swim'], 'name': 'tom'}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.25.0', 'X-Amzn-Trace-Id': 'Root=1-62852563-2fe77d4b1ce544696af60f10'}, 'origin': '113.87.15.99', 'url': 'http://httpbin.org/get?hobby=basketball&hobby=swim&name=tom&age=18'}
     
     response1 --> tom
    @@ -289,10 +295,10 @@ jsonpath5 --> basketball
             payload = {"hobby": ["basketball", "swim"], "name": "tom", "age": "18"}
             self.get("/get", params=payload)
             print("response \\n", self.response)
    -        
    +
             schema = genson(self.response)
             print("json Schema \\n", schema)
    -        
    +
             self.assertSchema(schema)
     
    • 运行日志
    ...
     response
    @@ -300,18 +306,17 @@ response
     
     json Schema
      {'$schema': 'http://json-schema.org/schema#', 'type': 'object', 'properties': {'args': {'type': 'object', 'properties': {'age': {'type': 'string'}, 'hobby': {'type': 'array', 'items': {'type': 'string'}}, 'name': {'type': 'string'}}, 'required': ['age', 'hobby', 'name']}, 'headers': {'type': 'object', 'properties': {'Accept': {'type': 'string'}, 'Accept-Encoding': {'type': 'string'}, 'Host': {'type': 'string'}, 'User-Agent': {'type': 'string'}, 'X-Amzn-Trace-Id': {'type': 'string'}}, 'required': ['Accept', 'Accept-Encoding', 'Host', 'User-Agent', 'X-Amzn-Trace-Id']}, 'origin': {'type': 'string'}, 'url': {'type': 'string'}}, 'required': ['args', 'headers', 'origin', 'url']}
    -

    mock URL

    seldom 3.2.3 支持

    seldom 运行允许通过confrun.py文件中mock_url() 配置mock URL映射。

    • confrun.py

    配置要映射的mock URL。

    ...
    -
    +

    mock URL

    seldom 3.2.3 支持

    seldom 运行允许通过confrun.py文件中mock_url() 配置mock URL映射。

    • confrun.py 配置要映射的mock URL。
    
     def mock_url():
         """
    -
    +    mock url
         :return:
         """
         config = {
             "http://httpbin.org/get": "http://127.0.0.1:8000/api/data",
         }
         return config
    -
    • test_api.py
    import seldom
    +
    • test_api.py
    import seldom
     
     
     class TestRequest(seldom.TestCase):
    @@ -343,7 +348,38 @@ json Schema
     2023-07-30 14:47:08 | DEBUG    | request.py | [response]:
      [{'item_name': 'apple'}, {'item_name': 'banana'}, {'item_name': 'orange'}, {'item_name': 'watermelon'}, {'item_name': 'grape'}]
     2023-07-30 14:47:08 | INFO     | case.py | 👀 assertStatusCode -> 200.
    -

    通过日志可以看到 http://httpbin.org/get 替换成为 http://127.0.0.1:8000/api/data 执行。 当你不想mock的时候只需要修改 mock_url() 即可,对于用例来说无影响。

    @retry装饰器

    @retry() 装饰器用于用法失败充实,例如封装的登录方法,允许API调用失败后再次尝试。

    示例如下:

    from seldom.request import HttpRequest
    +

    通过日志可以看到 http://httpbin.org/get 替换成为 http://127.0.0.1:8000/api/data 执行。 当你不想mock的时候只需要修改 mock_url() 即可,对于用例来说无影响。

    配置proxies代理

    seldom 3.11.0

    单个方法设置代理

    seldom 支持在每个请求方法中设置代理。

    import seldom
    +
    +
    +class TestHttpAssert(seldom.TestCase):
    +
    +    def test_assert_json(self):
    +        """
    +        test assertJSON
    +        """
    +        payload = {"name": "tom", "hobby": ["basketball", "swim"]}
    +        proxies = {
    +            "https": "http://localhost:1080",
    +            "http": "http://localhost:1080",
    +        }
    +        self.get("/get", params=payload, proxies=proxies)
    +

    全局设置代理

    当我们要所有用例都使用代理时,每个方法都单独设置就很麻烦了,可以使用confrun.py全局设置。

    • 目录结构
    ├───reports
    +├───test_data
    +├───test_dir
    +│   ├───...
    +├───confrun.py # 配置文件
    +└───run.py
    +
    • confrun.py 配置要映射的mock URL。
    
    +def proxies():
    +    """
    +    http proxies
    +    """
    +    proxies_conf = {
    +        "https": "http://localhost:1080",
    +        "http": "http://localhost:1080",
    +    }
    +    return proxies_conf
    +

    通过run.py文件全局运行测试,这里的代理配置将作用于所有请求方法。

    @retry装饰器

    @retry() 装饰器用于用法失败充实,例如封装的登录方法,允许API调用失败后再次尝试。

    示例如下:

    from seldom.request import HttpRequest
     from seldom.request import check_response, retry
     
     
    @@ -393,4 +429,4 @@ Traceback (most recent call last"C:\\Users\\fnngj\\.virtualenvs\\seldom-wKum2rzm\\Lib\\site-packages\\requests\\models.py", line 439, in prepare_url
         raise MissingSchema(
     requests.exceptions.MissingSchema: Invalid URL '/error': No scheme supplied. Perhaps you meant https:///error?
    -

    从运行结果可以看到,调用接口重试了2次,如果仍然错误,抛出异常。

    `,23);function b(g,q){const t=e("ExternalLinkIcon");return o(),c("div",null,[k,n("p",null,[s("通过 "),d,s(" 断言时需要写JSON Schema,但是这个写起来需要学习成本,seldom集成了"),n("a",m,[s("GenSON"),l(t)]),s(" ,可以帮你自动生成。")]),v])}const y=p(r,[["render",b],["__file","more.html.vue"]]);export{y as default}; +

    从运行结果可以看到,调用接口重试了2次,如果仍然错误,抛出异常。

    `,34);function b(g,q){const t=e("ExternalLinkIcon");return o(),c("div",null,[k,n("p",null,[s("通过 "),d,s(" 断言时需要写JSON Schema,但是这个写起来需要学习成本,seldom集成了"),n("a",m,[s("GenSON"),l(t)]),s(" ,可以帮你自动生成。")]),v])}const y=p(r,[["render",b],["__file","more.html.vue"]]);export{y as default}; diff --git a/assets/more.html-cc1b2581.js b/assets/more.html-8b232a2d.js similarity index 67% rename from assets/more.html-cc1b2581.js rename to assets/more.html-8b232a2d.js index cbae1e7..853c022 100644 --- a/assets/more.html-cc1b2581.js +++ b/assets/more.html-8b232a2d.js @@ -1 +1 @@ -const e=JSON.parse('{"key":"v-5b62ef19","path":"/api-testing/more.html","title":"更多功能","lang":"en-US","frontmatter":{},"headers":[{"level":3,"title":"har to case","slug":"har-to-case","link":"#har-to-case","children":[]},{"level":3,"title":"swagger to case","slug":"swagger-to-case","link":"#swagger-to-case","children":[]},{"level":3,"title":"请求转 cURL","slug":"请求转-curl","link":"#请求转-curl","children":[]},{"level":3,"title":"接口数据依赖","slug":"接口数据依赖","link":"#接口数据依赖","children":[]},{"level":3,"title":"Session使用","slug":"session使用","link":"#session使用","children":[]},{"level":3,"title":"提取接口返回数据","slug":"提取接口返回数据","link":"#提取接口返回数据","children":[]},{"level":3,"title":"genson","slug":"genson","link":"#genson","children":[]},{"level":3,"title":"mock URL","slug":"mock-url","link":"#mock-url","children":[]},{"level":3,"title":"@retry装饰器","slug":"retry装饰器","link":"#retry装饰器","children":[]}],"git":{"updatedTime":1709563448000,"contributors":[{"name":"defnngj","email":"defnngj@gmail.com","commits":6}]},"filePathRelative":"api-testing/more.md"}');export{e as data}; +const e=JSON.parse('{"key":"v-5b62ef19","path":"/api-testing/more.html","title":"更多功能","lang":"en-US","frontmatter":{},"headers":[{"level":3,"title":"har to case","slug":"har-to-case","link":"#har-to-case","children":[]},{"level":3,"title":"swagger to case","slug":"swagger-to-case","link":"#swagger-to-case","children":[]},{"level":3,"title":"请求转 cURL","slug":"请求转-curl","link":"#请求转-curl","children":[]},{"level":3,"title":"接口数据依赖","slug":"接口数据依赖","link":"#接口数据依赖","children":[]},{"level":3,"title":"Session使用","slug":"session使用","link":"#session使用","children":[]},{"level":3,"title":"提取接口返回数据","slug":"提取接口返回数据","link":"#提取接口返回数据","children":[]},{"level":3,"title":"genson","slug":"genson","link":"#genson","children":[]},{"level":3,"title":"mock URL","slug":"mock-url","link":"#mock-url","children":[]},{"level":3,"title":"配置proxies代理","slug":"配置proxies代理","link":"#配置proxies代理","children":[]},{"level":3,"title":"@retry装饰器","slug":"retry装饰器","link":"#retry装饰器","children":[]}],"git":{"updatedTime":1734512971000,"contributors":[{"name":"defnngj","email":"defnngj@gmail.com","commits":7}]},"filePathRelative":"api-testing/more.md"}');export{e as data}; diff --git a/assets/more.html-f6bf5e51.js b/assets/more.html-f6bf5e51.js deleted file mode 100644 index 736c997..0000000 --- a/assets/more.html-f6bf5e51.js +++ /dev/null @@ -1,396 +0,0 @@ -import{_ as p,r as e,o,c,a as n,b as s,d as l,e as a}from"./app-72107ff1.js";const i="/image/fiddler.png",u="/image/fiddler2.png",r={},k=a('

    更多功能

    har to case

    对于不熟悉 Requests 库的人来说,通过Seldom来写接口测试用例还是会有一点难度。于是,seldom 提供了har 文件转 case 的命令。

    首先,打开fiddler 工具进行抓包,选中某一个请求。

    然后,选择菜单栏:file -> Export Sessions -> Selected Sessions...

    选择导出的文件格式。

    点击next 保存为demo.har 文件。

    最后,通过seldom -h2c 转为demo.py 脚本文件。

    > seldom -h2c demo.har
    -
    -2021-06-14 18:05:50 [INFO] Start to generate testcase.
    -2021-06-14 18:05:50 [INFO] created file: ...\\demo.py
    -

    demo.py 文件。

    import seldom
    -
    -
    -class TestRequest(seldom.TestCase):
    -
    -    def start(self):
    -        self.url = "http://httpbin.org/post"
    -
    -    def test_case(self):
    -        headers = {"User-Agent": "python-requests/2.25.0", "Accept-Encoding": "gzip, deflate", "Accept": "application/json", "Connection": "keep-alive", "Host": "httpbin.org", "Content-Length": "36", "Origin": "http://httpbin.org", "Content-Type": "application/json", "Cookie": "lang=zh"}
    -        cookies = {"lang": "zh"}
    -        self.post(self.url, json={"key1": "value1", "key2": "value2"}, headers=headers, cookies=cookies)
    -        self.assertStatusCode(200)
    -
    -
    -if __name__ == '__main__':
    -    seldom.main()
    -
    -

    swagger to case

    seldom 3.6 版本支持。

    seldom 提供了swaggercase 的命令。 使用 seldom -s2c 命令。

    > seldom -s2c swagger.json
    -
    -2024-03-04 00:02:22 | INFO     | core.py | Start to generate testcase.
    -2024-03-04 00:02:22 | INFO     | core.py | created file: ...\\swagger.py
    -

    将swagger文档转为 seldom 自动化测试用例。

    import seldom
    -
    -
    -class TestRequest(seldom.TestCase): 
    -    
    -    def test_pet_petId_uploadImage_api_post(self):
    -        url = f"https://petstore.swagger.io/pet/{petId}/uploadImage"
    -        params = {}
    -        headers = {}
    -        headers["Content-Type"] = "multipart/form-data"
    -        data = {"additionalMetadata": additionalMetadata, "file": file}
    -        r = self.post(url, headers=headers, params=params, data=data)
    -        print(r.status_code)
    -
    -    def test_pet_api_post(self):
    -        url = f"https://petstore.swagger.io/pet"
    -        params = {}
    -        headers = {}
    -        headers["Content-Type"] = "application/json"
    -        data = {}
    -        r = self.post(url, headers=headers, params=params, data=data)
    -        print(r.status_code)
    -

    需要注意的是,转换的seldom自动化测试用例有一些变量,需要用户根据实际情况进行定义。

    请求转 cURL

    seldom 支持将请求转成cCURL命令, 你可以方便的通过cURL命令执行,或者导入到其他接口工具,例如,postman 支持cURL命令导入。

    # test_http.py
    -import seldom
    -
    -
    -class TestRequest(seldom.TestCase):
    -    """
    -    http api test demo
    -    doc: https://requests.readthedocs.io/en/master/
    -    """
    -
    -    def test_get_curl(self):
    -        """
    -        test get curl
    -        """
    -        self.get('http://httpbin.org/get', params={'key': 'value'})
    -        curl = self.curl()
    -        print(curl)
    -        self.post('http://httpbin.org/post', data={'key': 'value'})
    -        curl = self.curl()
    -        print(curl)
    -
    -        # or
    -        r = self.delete('http://httpbin.org/delete', params={'key': 'value'})
    -        curl = self.curl(r.request)
    -        print(curl)
    -        r = self.put('http://httpbin.org/put', json={'key': 'value'}, headers={"token": "123"})
    -        curl = self.curl(r.request)
    -        print(curl)
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(debug=True)
    -
    • 日志结果
    > python test_http.py
    -
    -...
    -curl -X GET  'Content-Type: application/json'  -H 'token: 123' -d '{"key": "value"}' http://httpbin.org/get
    -
    -curl -X POST  'Content-Type: application/x-www-form-urlencoded' -H  -d key=value http://httpbin.org/post
    -
    -curl -X DELETE  'http://httpbin.org/delete?key=value'
    -
    -curl -X PUT  -H 'Content-Type: application/json' -H 'token: 123' -d '{"key": "value"}' http://httpbin.org/put
    -

    接口数据依赖

    在场景测试中,我们需要利用上一个接口的数据,调用下一个接口。

    • 简单的接口依赖
    import seldom
    -
    -class TestRespData(seldom.TestCase):
    -
    -    def test_data_dependency(self):
    -        """
    -        Test for interface data dependencies
    -        """
    -        headers = {"X-Account-Fullname": "bugmaster"}
    -        self.get("/get", headers=headers)
    -        self.assertStatusCode(200)
    -
    -        username = self.response["headers"]["X-Account-Fullname"]
    -        self.post("/post", data={'username': username})
    -        self.assertStatusCode(200)
    -

    seldom提供了self.response用于记录上个接口返回的结果,直接拿来用即可。

    • 封装接口依赖
    1. 创建公共模块
    # common.py
    -from seldom.request import check_response 
    -from seldom.request import HttpRequest
    -
    -
    -class Common(HttpRequest):
    -    
    -    @check_response(
    -        describe="获取登录用户名",
    -        status_code=200,
    -        ret="headers.Account",
    -        check={"headers.Host": "httpbin.org"},
    -        debug=True
    -    )
    -    def get_login_user(self):
    -        """
    -        调用接口获得用户名
    -        """
    -        headers = {"Account": "bugmaster"}
    -        r = self.get("http://httpbin.org/get", headers=headers)
    -        return r
    -
    -
    -if __name__ == '__main__':
    -    c = Common()
    -    c.get_login_user()
    -
    • 运行日志
    2023-02-14 23:51:48 request.py | DEBUG | Execute get_login_user - args: (<__main__.Common object at 0x0000023263075100>,)
    -2023-02-14 23:51:48 request.py | DEBUG | Execute get_login_user - kwargs: {}
    -2023-02-14 23:51:48 request.py | INFO | -------------- Request -----------------[🚀]
    -2023-02-14 23:51:48 request.py | INFO | [method]: GET      [url]: http://httpbin.org/get
    -2023-02-14 23:51:48 request.py | DEBUG | [headers]:
    - {
    -  "Account": "bugmaster"
    -}
    -2023-02-14 23:51:49 request.py | INFO | -------------- Response ----------------[🛬️]
    -2023-02-14 23:51:49 request.py | INFO | successful with status 200
    -2023-02-14 23:51:49 request.py | DEBUG | [type]: json      [time]: 0.601097
    -2023-02-14 23:51:49 request.py | DEBUG | [response]:
    - {
    -  "args": {},
    -  "headers": {
    -    "Accept": "*/*",
    -    "Accept-Encoding": "gzip, deflate",
    -    "Account": "bugmaster",
    -    "Host": "httpbin.org",
    -    "User-Agent": "python-requests/2.28.1",
    -    "X-Amzn-Trace-Id": "Root=1-63ebae14-1e629b132c21f68e23ffeb33"
    -  },
    -  "origin": "173.248.248.88",
    -  "url": "http://httpbin.org/get"
    -}
    -2023-02-14 23:51:49 request.py | DEBUG | Execute get_login_user - response:
    - {'args': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Account': 'bugmaster', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.28.1', 'X-Amzn-Trace-Id': 'Root=1-63ebae14-1e629b132c21f68e23ffeb33'}, 'origin': '173.248.248.88', 'url': 'http://httpbin.org/get'}
    -2023-02-14 23:51:49 request.py | INFO | Execute get_login_user - 获取登录用户名 success!
    -

    @check_response 专门用于处理封装的方法。

    参数说明:

    • describe : 封装方法描述。
    • status_code: 判断接口返回的 HTTP 状态码,默认200
    • ret: 提取接口返回的字段,参考jmespath 提取规则。
    • check: 检查接口返回的字段。参考jmespath 提取规则。
    • debug: 开启debug,打印更多信息。
    1. 引用公共模块
    import seldom
    -from common import Common
    -
    -
    -class TestRequest(seldom.TestCase):
    -
    -    def start(self):
    -        self.c = Common()
    -
    -    def test_case(self):
    -        # 调用 get_login_user() 获取
    -        user = self.c.get_login_user()
    -        self.post("http://httpbin.org/post", data={'username': user})
    -        self.assertStatusCode(200)
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(debug=True)
    -
    -

    Session使用

    在实际测试过程中,大部分接口需要登录,Session 是一种非常简单记录登录状态的方式。

    import seldom
    -
    -
    -class TestCase(seldom.TestCase):
    -
    -    def start(self):
    -        self.s = self.Session()
    -        self.s.get('/cookies/set/sessioncookie/123456789')
    -
    -    def test_get_cookie1(self):
    -        self.s.get('/cookies')
    -
    -    def test_get_cookie2(self):
    -        self.s.get('/cookies')
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(debug=True, base_url="https://httpbin.org")
    -

    用法非常简单,你只需要在每个接口之前调用一次登录self.s对象就记录下了登录状态,通过self.s 再去调用其他接口就不需要登录。

    提取接口返回数据

    当接口返回的数据比较复杂时,我们需要有更方便方式去提取数据,seldom提供 jmespathjsonpath 来简化数据提取。

    • 接口返回数据
    {
    -  "args": {
    -    "hobby": [
    -      "basketball",
    -      "swim"
    -    ],
    -    "name": "tom"
    -  },
    -  "headers": {
    -    "Accept": "*/*",
    -    "Accept-Encoding": "gzip, deflate",
    -    "Host": "httpbin.org",
    -    "User-Agent": "python-requests/2.25.0",
    -    "X-Amzn-Trace-Id": "Root=1-62851614-1ca9fdb276238c60406c118f"
    -  },
    -  "origin": "113.87.15.99",
    -  "url": "http://httpbin.org/get?name=tom&hobby=basketball&hobby=swim"
    -}
    -
    • 常规提取
    import seldom
    -
    -
    -class TestAPI(seldom.TestCase):
    -
    -    def test_extract_responses(self):
    -        """
    -        提取 response 数据
    -        """
    -        payload = {"hobby": ["basketball", "swim"], "name": "tom", "age": "18"}
    -        self.get("http://httpbin.org/get", params=payload)
    -
    -        # response
    -        response1 = self.response["args"]["name"]
    -        response2 = self.response["args"]["hobby"]
    -        response3 = self.response["args"]["hobby"][0]
    -        print(f"response1 --> {response1}")
    -        print(f"response2 --> {response2}")
    -        print(f"response3 --> {response3}")
    -
    -        # jmespath
    -        jmespath1 = self.jmespath("args.name")
    -        jmespath2 = self.jmespath("args.hobby")
    -        jmespath3 = self.jmespath("args.hobby[0]")
    -        jmespath4 = self.jmespath("hobby[0]", response=self.response["args"])
    -        print(f"\\njmespath1 --> {jmespath1}")
    -        print(f"jmespath2 --> {jmespath2}")
    -        print(f"jmespath3 --> {jmespath3}")
    -        print(f"jmespath4 --> {jmespath4}")
    -
    -        # jsonpath
    -        jsonpath1 = self.jsonpath("$..name")
    -        jsonpath2 = self.jsonpath("$..hobby")
    -        jsonpath3 = self.jsonpath("$..hobby[0]")
    -        jsonpath4 = self.jsonpath("$..hobby[0]", index=0)
    -        jsonpath5 = self.jsonpath("$..hobby[0]", index=0, response=self.response["args"])
    -        print(f"\\njsonpath1 --> {jsonpath1}")
    -        print(f"jsonpath2 --> {jsonpath2}")
    -        print(f"jsonpath3 --> {jsonpath3}")
    -        print(f"jsonpath4 --> {jsonpath4}")
    -        print(f"jsonpath5 --> {jsonpath5}")
    -...
    -

    说明:

    • response: 保存接口返回的数据,可以直接以,字典列表的方式提取。
    • jmespath(): 根据 JMESPath 语法规则,默认提取接口返回的数据,也可指定resposne数据提取。
    • jsonpath(): 根据 JsonPath 语法规则,默认提取接口返回的数据, index指定下标,也可指定resposne数据提取。

    运行结果:

    2022-05-19 00:57:08 log.py | DEBUG | [response]:
    - {'args': {'age': '18', 'hobby': ['basketball', 'swim'], 'name': 'tom'}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.25.0', 'X-Amzn-Trace-Id': 'Root=1-62852563-2fe77d4b1ce544696af60f10'}, 'origin': '113.87.15.99', 'url': 'http://httpbin.org/get?hobby=basketball&hobby=swim&name=tom&age=18'}
    -
    -response1 --> tom
    -response2 --> ['basketball', 'swim']
    -response3 --> basketball
    -
    -jmespath1 --> tom
    -jmespath2 --> ['basketball', 'swim']
    -jmespath3 --> basketball
    -jmespath4 --> basketball
    -
    -jsonpath1 --> ['tom']
    -jsonpath2 --> [['basketball', 'swim']]
    -jsonpath3 --> ['basketball']
    -jsonpath4 --> basketball
    -jsonpath5 --> basketball
    -

    运行结果

    ...
    -2022-04-10 21:05:17.683 | DEBUG    | seldom.logging.log:debug:34 - [response]:
    - {'args': {'age': '18', 'hobby': ['basketball', 'swim'], 'name': 'tom'}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.25.0', 'X-Amzn-Trace-Id': 'Root=1-6252d60c-551433d744b6869e5d1944d7'}, 'origin': '113.87.12.14', 'url': 'http://httpbin.org/get?hobby=basketball&hobby=swim&name=tom&age=18'}
    -
    -2022-04-10 21:05:17.686 | DEBUG    | seldom.logging.log:debug:34 - [jresponse]:
    - ['basketball']
    -2022-04-10 21:05:17.689 | DEBUG    | seldom.logging.log:debug:34 - [jresponse]:
    - ['18']
    -

    genson

    `,57),d=n("code",null,"assertSchema()",-1),m={href:"https://github.com/wolverdude/GenSON",target:"_blank",rel:"noopener noreferrer"},v=a(`
    • 例子
    import seldom
    -from seldom.utils import genson
    -
    -
    -class TestAPI(seldom.TestCase):
    -
    -    def test_assert_schema(self):
    -        payload = {"hobby": ["basketball", "swim"], "name": "tom", "age": "18"}
    -        self.get("/get", params=payload)
    -        print("response \\n", self.response)
    -        
    -        schema = genson(self.response)
    -        print("json Schema \\n", schema)
    -        
    -        self.assertSchema(schema)
    -
    • 运行日志
    ...
    -response
    - {'args': {'age': '18', 'hobby': ['basketball', 'swim'], 'name': 'tom'}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.25.0', 'X-Amzn-Trace-Id': 'Root=1-626574d0-4c04bb7e76a53e8042c9d856'}, 'origin': '173.248.248.88', 'url': 'http://httpbin.org/get?hobby=basketball&hobby=swim&name=tom&age=18'}
    -
    -json Schema
    - {'$schema': 'http://json-schema.org/schema#', 'type': 'object', 'properties': {'args': {'type': 'object', 'properties': {'age': {'type': 'string'}, 'hobby': {'type': 'array', 'items': {'type': 'string'}}, 'name': {'type': 'string'}}, 'required': ['age', 'hobby', 'name']}, 'headers': {'type': 'object', 'properties': {'Accept': {'type': 'string'}, 'Accept-Encoding': {'type': 'string'}, 'Host': {'type': 'string'}, 'User-Agent': {'type': 'string'}, 'X-Amzn-Trace-Id': {'type': 'string'}}, 'required': ['Accept', 'Accept-Encoding', 'Host', 'User-Agent', 'X-Amzn-Trace-Id']}, 'origin': {'type': 'string'}, 'url': {'type': 'string'}}, 'required': ['args', 'headers', 'origin', 'url']}
    -

    mock URL

    seldom 3.2.3 支持

    seldom 运行允许通过confrun.py文件中mock_url() 配置mock URL映射。

    • confrun.py

    配置要映射的mock URL。

    ...
    -
    -def mock_url():
    -    """
    -
    -    :return:
    -    """
    -    config = {
    -        "http://httpbin.org/get": "http://127.0.0.1:8000/api/data",
    -    }
    -    return config
    -
    • test_api.py
    import seldom
    -
    -
    -class TestRequest(seldom.TestCase):
    -    """
    -    http api test demo
    -    """
    -
    -    def test_get_method(self):
    -        payload = {'key1': 'value1', 'key2': 'value2'}
    -        self.get("/get", params=payload)
    -        self.assertStatusCode(200)
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(base_url="http://httpbin.org")
    -
    • 运行
    > python test_api.py
    -
    -2023-07-30 14:47:08 | INFO     | request.py | -------------- Request -----------------[🚀]
    -2023-07-30 14:47:08 | INFO     | request.py | [method]: GET      [url]: http://httpbin.org/get
    -2023-07-30 14:47:08 | DEBUG    | request.py | [params]:
    -{
    -  "key1": "value1",
    -  "key2": "value2"
    -}
    -2023-07-30 14:47:08 | DEBUG    | request.py | mock url: http://127.0.0.1:8000/api/data
    -2023-07-30 14:47:08 | INFO     | request.py | -------------- Response ----------------[🛬️]
    -2023-07-30 14:47:08 | INFO     | request.py | successful with status 200
    -2023-07-30 14:47:08 | DEBUG    | request.py | [type]: json      [time]: 0.002738
    -2023-07-30 14:47:08 | DEBUG    | request.py | [response]:
    - [{'item_name': 'apple'}, {'item_name': 'banana'}, {'item_name': 'orange'}, {'item_name': 'watermelon'}, {'item_name': 'grape'}]
    -2023-07-30 14:47:08 | INFO     | case.py | 👀 assertStatusCode -> 200.
    -

    通过日志可以看到 http://httpbin.org/get 替换成为 http://127.0.0.1:8000/api/data 执行。 当你不想mock的时候只需要修改 mock_url() 即可,对于用例来说无影响。

    @retry装饰器

    @retry() 装饰器用于用法失败充实,例如封装的登录方法,允许API调用失败后再次尝试。

    示例如下:

    from seldom.request import HttpRequest
    -from seldom.request import check_response, retry
    -
    -
    -class LoginAPIObject(HttpRequest):
    -
    -    @retry(times=2, wait=3)
    -    @check_response(ret="form.token")
    -    def user_login(self, username: str, password: str) -> str:
    -        """
    -        模拟:登录API
    -        """
    -        params = {"username": username, "token": password}
    -        r = self.post("/error", json=params)
    -        return r
    -
    -
    -if __name__ == '__main__':
    -    login = LoginAPIObject()
    -    login.user_login("tom", "abc123")
    -
    • @retry()装饰器,times参数指定重试次数,默认3次,wait参数指定重试间隔,默认1s

    • @retry()装饰器可以单独使用,也可以和 @check_response()装饰器一起使用,如果一起使用的话,需要在上方。

    运行结果:

    2024-03-04 22:36:09 | INFO     | request.py | -------------- Request -----------------[🚀]
    -2024-03-04 22:36:09 | INFO     | request.py | [method]: POST      [url]: /error
    -2024-03-04 22:36:09 | DEBUG    | request.py | [json]:
    -{
    -  "username": "tom",
    -  "token": "abc123"
    -}
    -2024-03-04 22:36:09 | WARNING  | request.py | Attempt to execute <user_login> failed with error: 'Invalid URL '/error': No scheme supplied. Perhaps you meant https:///error?'. Attempting retry number 1...
    -2024-03-04 22:36:12 | INFO     | request.py | -------------- Request -----------------[🚀]
    -2024-03-04 22:36:12 | INFO     | request.py | [method]: POST      [url]: /error
    -2024-03-04 22:36:12 | DEBUG    | request.py | [json]:
    -{
    -  "username": "tom",
    -  "token": "abc123"
    -}
    -2024-03-04 22:36:12 | WARNING  | request.py | Attempt to execute <user_login> failed with error: 'Invalid URL '/error': No scheme supplied. Perhaps you meant https:///error?'. Attempting retry number 2...
    -2024-03-04 22:36:15 | INFO     | request.py | -------------- Request -----------------[🚀]
    -2024-03-04 22:36:15 | INFO     | request.py | [method]: POST      [url]: /error
    -2024-03-04 22:36:15 | DEBUG    | request.py | [json]:
    -{
    -  "username": "tom",
    -  "token": "abc123"
    -}
    -Traceback (most recent call last):
    -  File "D:\\github\\seldom\\api\\auth_object.py", line 20, in <module>
    -    login.user_login("tom", "abc123")
    -  ....
    -  File "C:\\Users\\fnngj\\.virtualenvs\\seldom-wKum2rzm\\Lib\\site-packages\\requests\\models.py", line 439, in prepare_url
    -    raise MissingSchema(
    -requests.exceptions.MissingSchema: Invalid URL '/error': No scheme supplied. Perhaps you meant https:///error?
    -

    从运行结果可以看到,调用接口重试了2次,如果仍然错误,抛出异常。

    `,23);function b(g,q){const t=e("ExternalLinkIcon");return o(),c("div",null,[k,n("p",null,[s("通过 "),d,s(" 断言时需要写JSON Schema,但是这个写起来需要学习成本,seldom集成了"),n("a",m,[s("GenSON"),l(t)]),s(" ,可以帮你自动生成。")]),v])}const y=p(r,[["render",b],["__file","more.html.vue"]]);export{y as default}; diff --git a/assets/other.html-8f9d5c4b.js b/assets/other.html-6774d738.js similarity index 99% rename from assets/other.html-8f9d5c4b.js rename to assets/other.html-6774d738.js index f503509..9b36843 100644 --- a/assets/other.html-8f9d5c4b.js +++ b/assets/other.html-6774d738.js @@ -1,4 +1,4 @@ -import{_ as t,r as o,o as p,c as i,a as n,b as s,d as c,e as a}from"./app-72107ff1.js";const l={},u=a(`

    浏览器启动配置

    selenium 在启动浏览器的时候可以做很多配置,seldom 试图简化这些配置,但是总有很多情况兼顾不到。

    seldom 3.2 版本开放了这些配置,你只需要将配置传给 seldom 即可。

    使用headless模式

    Firefox和Chrome浏览器支持headless模式,将浏览器置于后台运行,这样不会影响到我们在测试机上完成其他工作。

    • chrome
    import seldom
    +import{_ as t,r as o,o as p,c as i,a as n,b as s,d as c,e as a}from"./app-9fb6f1b5.js";const l={},u=a(`

    浏览器启动配置

    selenium 在启动浏览器的时候可以做很多配置,seldom 试图简化这些配置,但是总有很多情况兼顾不到。

    seldom 3.2 版本开放了这些配置,你只需要将配置传给 seldom 即可。

    使用headless模式

    Firefox和Chrome浏览器支持headless模式,将浏览器置于后台运行,这样不会影响到我们在测试机上完成其他工作。

    • chrome
    import seldom
     from selenium.webdriver import ChromeOptions
     
     # ...
    diff --git a/assets/other.html-e43e8fc0.js b/assets/other.html-e43e8fc0.js
    deleted file mode 100644
    index b689646..0000000
    --- a/assets/other.html-e43e8fc0.js
    +++ /dev/null
    @@ -1,162 +0,0 @@
    -import{_ as t,r as o,o as p,c as i,a as n,b as s,d as c,e as a}from"./app-a06a2d51.js";const l={},u=a(`

    浏览器启动配置

    selenium 在启动浏览器的时候可以做很多配置,seldom 试图简化这些配置,但是总有很多情况兼顾不到。

    seldom 3.2 版本开放了这些配置,你只需要将配置传给 seldom 即可。

    使用headless模式

    Firefox和Chrome浏览器支持headless模式,将浏览器置于后台运行,这样不会影响到我们在测试机上完成其他工作。

    • chrome
    import seldom
    -from selenium.webdriver import ChromeOptions
    -
    -# ...
    -
    -if __name__ == '__main__':
    -    chrome_options = ChromeOptions()
    -    chrome_options.add_argument("--headless=new")  # 开启 headless 模式
    -    browser = {
    -        "browser": "chrome",
    -        "options": chrome_options
    -    }
    -    seldom.main(browser=browser)
    -
    • firefox
    import seldom
    -from selenium.webdriver import FirefoxOptions
    -
    -# ...
    -
    -if __name__ == '__main__':
    -    firefox_options = FirefoxOptions()
    -    firefox_options.add_argument("-headless")  # 开启 headless 模式
    -    browser = {
    -        "browser": "firefox",
    -        "options": firefox_options
    -    }
    -    seldom.main(browser=browser)
    -
    • edge
    import seldom
    -from selenium.webdriver import EdgeOptions
    -
    -# ...
    -
    -if __name__ == '__main__':
    -    edge_option = EdgeOptions()
    -    edge_option.add_argument("--headless=new")
    -
    -    browser = {
    -        "browser": "edge",
    -        "options": edge_option
    -    }
    -    seldom.main(browser=browser)
    -

    Selenium Grid

    首先,安装Java环境,然后下载 selenium-server

    > java -jar .\\selenium-server-4.12.0.jar standalone
    -
    -23:17:59.476 INFO [LoggingOptions.configureLogEncoding] - Using the system default encoding
    -23:17:59.481 INFO [OpenTelemetryTracer.createTracer] - Using OpenTelemetry for tracing
    -23:18:03.933 INFO [NodeOptions.getSessionFactories] - Detected 16 available processors
    -23:18:03.935 INFO [NodeOptions.discoverDrivers] - Looking for existing drivers on the PATH.
    -23:18:03.935 INFO [NodeOptions.discoverDrivers] - Add '--selenium-manager true' to the startup command to setup drivers automatically.
    -23:18:04.971 INFO [SeleniumManager.lambda$runCommand$1] - Driver path: C:\\webdriver\\chromedriver.exe
    -23:18:04.971 INFO [SeleniumManager.lambda$runCommand$1] - Browser path: C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe
    -23:18:05.469 INFO [SeleniumManager.lambda$runCommand$1] - Driver path: D:\\webdriver\\msedgedriver.exe
    -23:18:05.470 INFO [SeleniumManager.lambda$runCommand$1] - Browser path: C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe
    -23:18:05.847 INFO [SeleniumManager.lambda$runCommand$1] - Driver path: C:\\Users\\fnngj\\.cache\\selenium\\geckodriver\\win64\\0.33.0\\geckodriver.exe
    -23:18:05.848 INFO [SeleniumManager.lambda$runCommand$1] - Browser path: C:\\Program Files\\Mozilla Firefox\\firefox.exe
    -23:18:06.223 WARN [SeleniumManager.lambda$runCommand$1] - Unable to discover proper IEDriverServer version in offline mode
    -23:18:06.246 INFO [NodeOptions.report] - Adding Edge for {"browserName": "MicrosoftEdge","ms:edgeOptions": {"args": ["--remote-allow-origins=*"]},"platformName": "Windows 11"} 16 times
    -23:18:06.247 INFO [NodeOptions.report] - Adding Firefox for {"browserName": "firefox","platformName": "Windows 11"} 16 times
    -23:18:06.249 INFO [NodeOptions.report] - Adding Chrome for {"browserName": "chrome","goog:chromeOptions": {"args": ["--remote-allow-origins=*"]},"platformName": "Windows 11"} 16 times
    -23:18:06.343 INFO [Node.<init>] - Binding additional locator mechanisms: relative
    -23:18:06.360 INFO [GridModel.setAvailability] - Switching Node 35d0ca88-221c-4dba-8ad5-08b20a1280fc (uri: http://192.168.0.202:4444) from DOWN to UP
    -23:18:06.361 INFO [LocalDistributor.add] - Added node 35d0ca88-221c-4dba-8ad5-08b20a1280fc at http://192.168.0.202:4444. Health check every 120s
    -23:18:07.915 INFO [Standalone.execute] - Started Selenium Standalone 4.12.0 (revision 249f2a7d1b*): http://192.168.0.202:4444
    -
    import seldom
    -from selenium.webdriver import ChromeOptions
    -
    -# ……
    -
    -if __name__ == '__main__':
    -    chrome_options = ChromeOptions()
    -    browser = {
    -        "options": chrome_options,  # chrome浏览器配置,其他类似
    -        "command_executor": "http://192.168.0.202:4444",
    -    }
    -    seldom.main(browser=browser)
    -
    `,15),r={href:"https://www.selenium.dev/documentation/grid/getting_started/",target:"_blank",rel:"noopener noreferrer"},d=a(`

    Mobile Web 模式

    seldom 还支持 Mobile web 模式:

    import seldom
    -from selenium.webdriver import ChromeOptions
    -
    -# ...
    -
    -if __name__ == '__main__':
    -    chrome_options = ChromeOptions()
    -    chrome_options.add_experimental_option("mobileEmulation", {"deviceName": "iPhone 8"})
    -
    -    browser = {
    -        "browser": "chrome",
    -        "options": chrome_options,
    -    }
    -    seldom.main(debug=True, browser=browser)
    -
    • deviceName: 指定移动设备的型号。

    移动设备通过通过 浏览器开发者工具 查看,参考型号: 'iPhone 8', 'iPhone 8 Plus', 'iPhone SE', 'iPhone X', 'iPhone XR', 'iPhone 12 Pro', 'Pixel 2', 'Pixel XL', 'Pixel 5', 'Samsung Galaxy S8+', 'Samsung Galaxy S20 Ultra', 'iPad Air', 'iPad Pro', 'iPad Mini'。

    浏览器忽略无效证书

    import seldom
    -from selenium.webdriver import ChromeOptions
    -
    -# ...
    -
    -if __name__ == '__main__':
    -    chrome_options = ChromeOptions()
    -    chrome_options.add_argument('--ignore-certificate-errors')  # 忽略无效证书的问题
    -    browser = {
    -        "browser": "chrome",
    -        "options": chrome_options,
    -    }
    -    seldom.main(browser=browser)
    -

    浏览器关闭沙盒模式

    import seldom
    -from selenium.webdriver import ChromeOptions
    -
    -# ...
    -
    -if __name__ == '__main__':
    -    chrome_options = ChromeOptions()
    -    chrome_options.add_argument('--no-sandbox')  # 关闭沙盒模式
    -    browser = {
    -        "browser": "chrome",
    -        "options": chrome_options,
    -    }
    -    seldom.main(browser=browser)
    -

    开启实验性功能

    chrome开启实验性功能参数 excludeSwitches

    
    -import seldom
    -from selenium.webdriver import ChromeOptions
    -
    -# ...
    -
    -if __name__ == '__main__':
    -    chrome_options = ChromeOptions()
    -    chrome_options.add_experimental_option("excludeSwitches", ['enable-automation', 'enable-logging'])
    -    browser = {
    -        "browser": "chrome",
    -        "options": chrome_options,
    -    }
    -    seldom.main(browser=browser)
    -

    设置浏览器代理

    import seldom
    -from selenium.webdriver import ChromeOptions
    -
    -# ...
    -
    -if __name__ == '__main__':
    -    proxy = "127.0.0.1:1080"  # 示例代理地址和端口
    -
    -    chrome_options = ChromeOptions()
    -    chrome_options.add_argument(f"--proxy-server={proxy}")
    -    browser = {
    -        "browser": "chrome",
    -        "options": chrome_options,
    -    }
    -    seldom.main(browser=browser)
    -

    连接已打开浏览器

    • 查看浏览器安装位置
    > selenium-manager.exe  --browser edge
    -[2024-10-08T03:50:40.000Z INFO ] Driver path: C:\\Users\\xx\\.cache\\selenium\\msedgedriver\\win64\\130.0.2849.13\\msedgedriver.exe
    -[2024-10-08T03:50:40.000Z INFO ] Browser path: C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe
    -
    • 启动浏览器
    msedge.exe  --remote-debugging-port=9527 --user-data-dir="D:\\webdriver\\edge"
    -

    --remote-debugging-port: 浏览器远程调试端口。

    --user-data-dir: 用户数据目录,创建一个空目录用于保存浏览器用户数据数据。

    • 启动浏览器指定端口
    import seldom
    -from selenium.webdriver import EdgeOptions
    -
    -# ...
    -
    -if __name__ == '__main__':
    -    option = EdgeOptions()
    -    # 设置连接已打开浏览器
    -    option.add_experimental_option("debuggerAddress", "127.0.0.1:9527")
    -    browser = {
    -        "browser": "edge",
    -        "options": option
    -    }
    -    seldom.main(browser=browser)
    -
    -
    `,23);function k(m,v){const e=o("ExternalLinkIcon");return p(),i("div",null,[u,n("ul",null,[n("li",null,[s("设置远程节点,"),n("a",r,[s("selenium Grid doc"),c(e)]),s("。")])]),d])}const g=t(l,[["render",k],["__file","other.html.vue"]]);export{g as default}; diff --git a/assets/page_object.html-c3c4d18b.js b/assets/page_object.html-90234acd.js similarity index 99% rename from assets/page_object.html-c3c4d18b.js rename to assets/page_object.html-90234acd.js index 6cc66f5..1eba069 100644 --- a/assets/page_object.html-c3c4d18b.js +++ b/assets/page_object.html-90234acd.js @@ -1,4 +1,4 @@ -import{_ as n,o as s,c as a,e as t}from"./app-72107ff1.js";const e={},p=t(`

    Page Object

    在编写App自动化测试时,推荐使用page object models(简称 PO设计模式)。你可以看到seldom并没有完全封装appium的API,我们可以借助 poium 来实现基于元素的定位。

    github: https://github.com/SeldomQA/poium

    pip 安装

    > pip install poium
    +import{_ as n,o as s,c as a,e as t}from"./app-9fb6f1b5.js";const e={},p=t(`

    Page Object

    在编写App自动化测试时,推荐使用page object models(简称 PO设计模式)。你可以看到seldom并没有完全封装appium的API,我们可以借助 poium 来实现基于元素的定位。

    github: https://github.com/SeldomQA/poium

    pip 安装

    > pip install poium
     

    使用poium

    在seldom中基于poium实现元素的定位和操作。

    import seldom
     from seldom.appium_lab.android import UiAutomator2Options
     from poium import Page, Element, Elements
    diff --git a/assets/page_object.html-d5443023.js b/assets/page_object.html-bc99b214.js
    similarity index 98%
    rename from assets/page_object.html-d5443023.js
    rename to assets/page_object.html-bc99b214.js
    index dd2776c..e880462 100644
    --- a/assets/page_object.html-d5443023.js
    +++ b/assets/page_object.html-bc99b214.js
    @@ -1,4 +1,4 @@
    -import{_ as e,r as t,o,c as p,a as n,b as s,d as i,e as c}from"./app-a06a2d51.js";const l={},u=n("h1",{id:"page-object",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#page-object","aria-hidden":"true"},"#"),s(" Page Object")],-1),r=n("p",null,[s("seldom API 的设计理念是将元素操作和元素定位放到起,本身不太适合实现"),n("code",null,"Page object"),s("设计模式。")],-1),d={href:"https://github.com/SeldomQA/poium",target:"_blank",rel:"noopener noreferrer"},k=n("code",null,"Page objects",-1),m=c(`
    • pip 安装
    > pip install poium
    +import{_ as e,r as t,o,c as p,a as n,b as s,d as i,e as c}from"./app-9fb6f1b5.js";const l={},u=n("h1",{id:"page-object",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#page-object","aria-hidden":"true"},"#"),s(" Page Object")],-1),r=n("p",null,[s("seldom API 的设计理念是将元素操作和元素定位放到起,本身不太适合实现"),n("code",null,"Page object"),s("设计模式。")],-1),d={href:"https://github.com/SeldomQA/poium",target:"_blank",rel:"noopener noreferrer"},k=n("code",null,"Page objects",-1),m=c(`
    • pip 安装
    > pip install poium
     

    将 seldom 与 poium 结合使用。

    import seldom
     from poium import Page, Element
     
    diff --git a/assets/page_object.html-e18b992a.js b/assets/page_object.html-e18b992a.js
    deleted file mode 100644
    index 6006a8e..0000000
    --- a/assets/page_object.html-e18b992a.js
    +++ /dev/null
    @@ -1,28 +0,0 @@
    -import{_ as e,r as t,o,c as p,a as n,b as s,d as i,e as c}from"./app-72107ff1.js";const l={},u=n("h1",{id:"page-object",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#page-object","aria-hidden":"true"},"#"),s(" Page Object")],-1),r=n("p",null,[s("seldom API 的设计理念是将元素操作和元素定位放到起,本身不太适合实现"),n("code",null,"Page object"),s("设计模式。")],-1),d={href:"https://github.com/SeldomQA/poium",target:"_blank",rel:"noopener noreferrer"},k=n("code",null,"Page objects",-1),m=c(`
    • pip 安装
    > pip install poium
    -

    将 seldom 与 poium 结合使用。

    import seldom
    -from poium import Page, Element
    -
    -
    -class BaiduPage(Page):
    -    """baidu page"""
    -    search_input = Element(id_="kw")
    -    search_button = Element(id_="su")
    -
    -
    -class BaiduTest(seldom.TestCase):
    -    """Baidu search test case"""
    -
    -    def test_case(self):
    -        """
    -        A simple test
    -        """
    -        page = BaiduPage(self.driver, print_log=True)
    -        page.open("https://www.baidu.com")
    -        page.search_input.send_keys("seldom")
    -        page.search_button.click()
    -        self.assertTitle("seldom_百度搜索")
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(browser="chrome")
    -
    `,4);function v(b,_){const a=t("ExternalLinkIcon");return o(),p("div",null,[u,r,n("p",null,[n("a",d,[s("poium"),i(a)]),s(" 是"),k,s("设计模式最佳实践。")]),m])}const q=e(l,[["render",v],["__file","page_object.html.vue"]]);export{q as default}; diff --git a/assets/page_object.html-fd190005.js b/assets/page_object.html-fd190005.js deleted file mode 100644 index 16b05d8..0000000 --- a/assets/page_object.html-fd190005.js +++ /dev/null @@ -1,61 +0,0 @@ -import{_ as n,o as s,c as a,e as t}from"./app-a06a2d51.js";const e={},p=t(`

    Page Object

    在编写App自动化测试时,推荐使用page object models(简称 PO设计模式)。你可以看到seldom并没有完全封装appium的API,我们可以借助 poium 来实现基于元素的定位。

    github: https://github.com/SeldomQA/poium

    pip 安装

    > pip install poium
    -

    使用poium

    在seldom中基于poium实现元素的定位和操作。

    import seldom
    -from seldom.appium_lab.android import UiAutomator2Options
    -from poium import Page, Element, Elements
    -
    -
    -class BBSPage(Page):
    -    search_input = Element(id_="com.meizu.flyme.flymebbs:id/nw")
    -    search_button = Element(id_="com.meizu.flyme.flymebbs:id/o1")
    -    search_result = Elements(id_="com.meizu.flyme.flymebbs:id/a29")
    -
    -
    -class TestBBS(seldom.TestCase):
    -
    -    def start(self):
    -        self.bbs_page = BBSPage(self.driver)
    -
    -    def test_bbs(self):
    -        self.sleep(5)
    -        self.bbs_page.search_input.click()
    -        self.bbs_page.search_input.send_keys("flyme")
    -        self.bbs_page.search_button.click()
    -        elems = self.bbs_page.search_result
    -        for title in elems:
    -            self.assertIn("flyme", title.text.lower())
    -
    -
    -if __name__ == '__main__':
    -    # 定义运行App
    -    capabilities = {
    -        "automationName": "UiAutomator2",
    -        "platformName": "Android",
    -        "appPackage": "com.meizu.flyme.flymebbs",
    -        "appActivity": "com.meizu.myplus.ui.splash.SplashActivity",
    -        "noReset": True,
    -    }
    -    options = UiAutomator2Options().load_capabilities(capabilities)
    -    seldom.main(app_server="http://127.0.0.1:4723", app_info=options, debug=True)
    -

    定位方法

    poium 支持的定位方法。

    # selenium
    -css = "xx"
    -id_ = "xx"
    -name = "xx"
    -xpath = "xx"
    -link_text = "xx"
    -partial_link_text = "xx"
    -tag = "xx"
    -class_name = "xx"
    -
    -# appium
    -ios_uiautomation = "xx"
    -ios_predicate = "xx"
    -ios_class_chain = "xx"
    -android_uiautomator = "xx"
    -android_viewtag = "xx"
    -android_data_matcher = "xx"
    -android_view_matcher = "xx"
    -windows_uiautomation = "xx"
    -accessibility_id = "xx"
    -image = "xx"
    -custom = "xx"
    -

    Element类参数

    • timeout: 设置超时检查次数,默认为5。
    • index: 设置元素索引,当你的定位方式默认匹配到多个元素时,默认返回第1个,即为0.
    • describe: 设置元素描述,默认为undefined, 建议为每个元素增加描述。
    `,13),o=[p];function i(c,l){return s(),a("div",null,o)}const r=n(e,[["render",i],["__file","page_object.html.vue"]]);export{r as default}; diff --git a/assets/platform.html-3c6229ce.js b/assets/platform.html-3c6229ce.js new file mode 100644 index 0000000..1c31bfd --- /dev/null +++ b/assets/platform.html-3c6229ce.js @@ -0,0 +1,159 @@ +import{_ as n,o as s,c as a,e}from"./app-9fb6f1b5.js";const t={},o=e(`

    平台化支持

    为了更好的支持测试用例平台化,Seldom 提供了API用于获取用例列表,以及根据传入的用例信息运行测试用例。

    seldom-platform: https://github.com/SeldomQA/seldom-platform

    目录结构如下:

    mypro/
    +├── test_dir/
    +│   ├── module_api/
    +│   │   ├── test_http_demo.py
    +│   ├── module_web/
    +│   │   ├── test_first_demo.py
    +│   │   ├── test_ddt_demo.py
    +└── run.py
    +

    获取用例信息

    # run.py
    +from seldom import SeldomTestLoader
    +from seldom import TestMainExtend
    +
    +if __name__ == '__main__':
    +    SeldomTestLoader.collectCaseInfo = True
    +    main_extend = TestMainExtend(path="./test_dir/")
    +    case_info = main_extend.collect_cases(json=True)
    +    print(case_info)
    +

    说明

    返回的用例信息列表:

    • collectCaseInfocollectCaseInfo设置为True 说明需要收集用例信息。
    • TestMainExtend(path="./test_dir/")TestMainExtend类是TestMain类的扩展,path设置收集用例的目录,不能为空。
    • collect_cases(json=False, level="data", warning=False):返回收集的用例信息。
      • json=False:默认为list格式;设置为True返回json格式。
      • level="data" :默认为data,数据驱动的每条数据被解析为一条用例。如果设置为 method 数据驱动的方法被解析为一条用例。
      • warning=False: 默认为False, 在收集用例的过程中,因为缺少依赖库,或导包错误会导致部分用例收集报错,是否要将这些错误保存下来。开启(True)后,默认保存在reports/collect_warning.log 文件中。
    [
    +  {
    +    "file": "module_api.test_http_demo",
    +    "class": {
    +      "name": "TestRequest",
    +      "doc": "\\n    http api test demo\\n    doc: https://requests.readthedocs.io/en/master/\\n    "
    +    },
    +    "method": {
    +      "name": "test_get_method",
    +      "doc": "\\n        test get request\\n        ",
    +      "label": null
    +    }
    +  },
    +  {
    +    "file": "module_api.test_http_demo",
    +    "class": {
    +      "name": "TestRequest",
    +      "doc": "\\n    http api test demo\\n    doc: https://requests.readthedocs.io/en/master/\\n    "
    +    },
    +    "method": {
    +      "name": "test_post_method",
    +      "doc": "\\n        test post request\\n        ",
    +      "label": null
    +    }
    +  },
    +  {
    +    "file": "module_web.test_ddt_demo",
    +    "class": {
    +      "name": "BaiduTest",
    +      "doc": "Baidu search test case"
    +    },
    +    "method": {
    +      "name": "test_baidu_0",
    +      "doc": "used parameterized test [with name=1, search_key='seldom']\\n        :param name: case name\\n        :param search_key: search keyword\\n        ",
    +      "label": null
    +    }
    +  },
    +  {
    +    "file": "module_web.test_ddt_demo",
    +    "class": {
    +      "name": "BaiduTest",
    +      "doc": "Baidu search test case"
    +    },
    +    "method": {
    +      "name": "test_baidu_1",
    +      "doc": "used parameterized test [with name=2, search_key='selenium']\\n        :param name: case name\\n        :param search_key: search keyword\\n        ",
    +      "label": null
    +    }
    +  },
    +  {
    +    "file": "module_web.test_ddt_demo",
    +    "class": {
    +      "name": "BaiduTest",
    +      "doc": "Baidu search test case"
    +    },
    +    "method": {
    +      "name": "test_baidu_2",
    +      "doc": "used parameterized test [with name=3, search_key='unittest']\\n        :param name: case name\\n        :param search_key: search keyword\\n        ",
    +      "label": null
    +    }
    +  },
    +  {
    +    "file": "module_web.test_first_demo",
    +    "class": {
    +      "name": "BaiduTest",
    +      "doc": "Baidu search test case"
    +    },
    +    "method": {
    +      "name": "test_case",
    +      "doc": "a simple test case ",
    +      "label": null
    +    }
    +  }
    +]
    +

    数据结构说明:

    • file: 获取类的文件名,包含目录名。
    • class: 测试类的名字name 和 描述doc
    • method: 测试方法的名字name 和 描述doc, label

    注明:seldom==3.11.0 版本测试方法增加label字段。

    执行用例信息

    当获取用例信息之后,可以进行自定义,例如 挑选出需要执行的用例,重新传给Seldom 执行。

    # run.py
    +from seldom import TestMainExtend
    +
    +if __name__ == '__main__':
    +    # 自定义要执行的用例
    +    cases = [
    +        {
    +            "file": "module_web.test_first_demo",
    +            "class": {
    +                "name": "BaiduTest",
    +                "doc": "Baidu search test case"
    +            },
    +            "method": {
    +                "name": "test_case",
    +                "doc": "a simple test case ",
    +                "label": ""
    +            }
    +        }
    +    ]
    +    main_extend = TestMainExtend(path="./test_dir")
    +    main_extend.run_cases(cases)
    +

    说明:

    • cases 定义要执行的用例信息, doc 非必填字段。
    • TestMainExtend(path="./test_dir") : 其中path指定从哪个目录查找用例集合。
    • run_cases(cases): 运行用例。

    接入平台比读

    如果你只是使用seldom框架编写用例,那么代码只要框架能运行即可,如果要接入seldom-platform平台,那么需要注意一下几点。

    🚧 测试每个子目录必须包含__init__.py文件。

    • 目录结构
    ├───reports
    +├───test_data
    +├───test_dir
    +│   ├───api_case
    +│   │   └───__init__.py
    +│   ├───app_case
    +│   │   └───__init__.py
    +│   ├───web_case
    +│   │   └───__init__.py
    +│   └───__init__.py
    +└───run.py
    +

    如果子目录不添加 init.py 文件会导致目录下面的用例无法解析。

    🚧 用例的前置动作

    在用 seldom框架写用例的时候需要执行一些前置/后置动作。

    import seldom
    +from seldom.utils import cache
    +
    +if __name__ == '__main__':
    +    # 前置动作
    +    cache.set({"token": "token123"})
    +    # 执行用例
    +    seldom.main("./test_dir")
    +    # 后置动作
    +    cache.clear("token")
    +

    但是,平台执行的时候,不会执行 前置/后置动作。 那么,为了使平台可以执行前置动作,需要使用confrun.py文件进行配置。

    • 目录结构
    ├───reports
    +├───test_data
    +├───test_dir
    +│   ├───...
    +├───confrun.py
    +└───run.py
    +
    • confrun.py配置
    """
    +seldom confrun.py  hooks function
    +"""
    +from seldom.utils import cache
    +
    +
    +def start_run():
    +    """前置动作"""
    +    cache.set({"token": "token123"})
    +
    +
    +def end_run():
    +    """后置动作"""
    +    cache.clear("token")
    +
    • run.py文件
    import seldom
    +
    +if __name__ == '__main__':
    +    # 执行用例
    +    seldom.main("./test_dir")
    +

    通过上面的配置,前置、后置动作就可以在平台上运行,当然,这样设置本地也可正常运行。

    `,36),p=[o];function i(l,c){return s(),a("div",null,p)}const d=n(t,[["render",i],["__file","platform.html.vue"]]);export{d as default}; diff --git a/assets/platform.html-749be776.js b/assets/platform.html-749be776.js deleted file mode 100644 index 5221422..0000000 --- a/assets/platform.html-749be776.js +++ /dev/null @@ -1,106 +0,0 @@ -import{_ as s,o as n,c as a,e as t}from"./app-72107ff1.js";const e={},o=t(`

    平台化支持

    为了更好的支持测试用例平台化,Seldom 提供了API用于获取用例列表,以及根据传入的用例信息运行测试用例。

    目录结构如下:

    mypro/
    -├── test_dir/
    -│   ├── module_api/
    -│   │   ├── test_http_demo.py
    -│   ├── module_web/
    -│   │   ├── test_first_demo.py
    -│   │   ├── test_ddt_demo.py
    -└── run.py
    -

    获取用例信息

    # run.py
    -from seldom import SeldomTestLoader
    -from seldom import TestMainExtend
    -
    -if __name__ == '__main__':
    -    SeldomTestLoader.collectCaseInfo = True
    -    main_extend = TestMainExtend(path="./test_dir/")
    -    case_info = main_extend.collect_cases(json=True)
    -    print(case_info)
    -

    说明

    返回的用例信息列表:

    • collectCaseInfocollectCaseInfo设置为True 说明需要收集用例信息。
    • TestMainExtend(path="./test_dir/")TestMainExtend类是TestMain类的扩展,path设置收集用例的目录,不能为空。
    • collect_cases(json=False, level="data", warning=False):返回收集的用例信息。
      • json=False:默认为list格式;设置为True返回json格式。
      • level="data" :默认为data,数据驱动的每条数据被解析为一条用例。如果设置为 method 数据驱动的方法被解析为一条用例。
      • warning=False: 默认为False, 在收集用例的过程中,因为缺少依赖库,或导包错误会导致部分用例收集报错,是否要将这些错误保存下来。开启(True)后,默认保存在reports/collect_warning.log 文件中。
    [
    -    {
    -        "file": "module_api.test_http_demo",
    -        "class": {
    -            "name": "TestRequest",
    -            "doc": "\\n    http api test demo\\n    doc: https://requests.readthedocs.io/en/master/\\n    "
    -        },
    -        "method": {
    -            "name": "test_get_method",
    -            "doc": "\\n        test get request\\n        "
    -        }
    -    },
    -    {
    -        "file": "module_api.test_http_demo",
    -        "class": {
    -            "name": "TestRequest",
    -            "doc": "\\n    http api test demo\\n    doc: https://requests.readthedocs.io/en/master/\\n    "
    -        },
    -        "method": {
    -            "name": "test_post_method",
    -            "doc": "\\n        test post request\\n        "
    -        }
    -    },
    -    {
    -        "file": "module_web.test_ddt_demo",
    -        "class": {
    -            "name": "BaiduTest",
    -            "doc": "Baidu search test case"
    -        },
    -        "method": {
    -            "name": "test_baidu_0",
    -            "doc": "used parameterized test [with name=1, search_key='seldom']\\n        :param name: case name\\n        :param search_key: search keyword\\n        "
    -        }
    -    },
    -    {
    -        "file": "module_web.test_ddt_demo",
    -        "class": {
    -            "name": "BaiduTest",
    -            "doc": "Baidu search test case"
    -        },
    -        "method": {
    -            "name": "test_baidu_1",
    -            "doc": "used parameterized test [with name=2, search_key='selenium']\\n        :param name: case name\\n        :param search_key: search keyword\\n        "
    -        }
    -    },
    -    {
    -        "file": "module_web.test_ddt_demo",
    -        "class": {
    -            "name": "BaiduTest",
    -            "doc": "Baidu search test case"
    -        },
    -        "method": {
    -            "name": "test_baidu_2",
    -            "doc": "used parameterized test [with name=3, search_key='unittest']\\n        :param name: case name\\n        :param search_key: search keyword\\n        "
    -        }
    -    },
    -    {
    -        "file": "module_web.test_first_demo",
    -        "class": {
    -            "name": "BaiduTest",
    -            "doc": "Baidu search test case"
    -        },
    -        "method": {
    -            "name": "test_case",
    -            "doc": "a simple test case "
    -        }
    -    }
    -]
    -

    数据结构说明:

    • file: 获取类的文件名,包含目录名。
    • class: 测试类的名字name 和 描述doc
    • method: 测试方法的名字name 和 描述doc

    执行用例信息

    当获取用例信息之后,可以进行自定义,例如 挑选出需要执行的用例,重新传给Seldom 执行。

    # run.py
    -from seldom import TestMainExtend
    -
    -if __name__ == '__main__':
    -    # 自定义要执行的用例
    -    cases = [
    -        {
    -            "file": "module_web.test_first_demo",
    -            "class": {
    -                "name": "BaiduTest",
    -                "doc": "Baidu search test case"
    -            },
    -            "method": {
    -                "name": "test_case",
    -                "doc": "a simple test case "
    -            }
    -        }
    -    ]
    -    main_extend = TestMainExtend(path="./test_dir")
    -    main_extend.run_cases(cases)
    -

    说明:

    • cases 定义要执行的用例信息, doc 非必填字段。
    • TestMainExtend(path="./test_dir") : 其中path指定从哪个目录查找用例集合。
    • run_cases(cases): 运行用例。

    相关项目

    seldom-platform: https://github.com/SeldomQA/seldom-platform

    `,19),p=[o];function c(i,l){return n(),a("div",null,p)}const r=s(e,[["render",c],["__file","platform.html.vue"]]);export{r as default}; diff --git a/assets/platform.html-7e431a87.js b/assets/platform.html-7e431a87.js deleted file mode 100644 index 3e9f63b..0000000 --- a/assets/platform.html-7e431a87.js +++ /dev/null @@ -1,106 +0,0 @@ -import{_ as s,o as n,c as a,e as t}from"./app-a06a2d51.js";const e={},o=t(`

    平台化支持

    为了更好的支持测试用例平台化,Seldom 提供了API用于获取用例列表,以及根据传入的用例信息运行测试用例。

    目录结构如下:

    mypro/
    -├── test_dir/
    -│   ├── module_api/
    -│   │   ├── test_http_demo.py
    -│   ├── module_web/
    -│   │   ├── test_first_demo.py
    -│   │   ├── test_ddt_demo.py
    -└── run.py
    -

    获取用例信息

    # run.py
    -from seldom import SeldomTestLoader
    -from seldom import TestMainExtend
    -
    -if __name__ == '__main__':
    -    SeldomTestLoader.collectCaseInfo = True
    -    main_extend = TestMainExtend(path="./test_dir/")
    -    case_info = main_extend.collect_cases(json=True)
    -    print(case_info)
    -

    说明

    返回的用例信息列表:

    • collectCaseInfocollectCaseInfo设置为True 说明需要收集用例信息。
    • TestMainExtend(path="./test_dir/")TestMainExtend类是TestMain类的扩展,path设置收集用例的目录,不能为空。
    • collect_cases(json=False, level="data", warning=False):返回收集的用例信息。
      • json=False:默认为list格式;设置为True返回json格式。
      • level="data" :默认为data,数据驱动的每条数据被解析为一条用例。如果设置为 method 数据驱动的方法被解析为一条用例。
      • warning=False: 默认为False, 在收集用例的过程中,因为缺少依赖库,或导包错误会导致部分用例收集报错,是否要将这些错误保存下来。开启(True)后,默认保存在reports/collect_warning.log 文件中。
    [
    -    {
    -        "file": "module_api.test_http_demo",
    -        "class": {
    -            "name": "TestRequest",
    -            "doc": "\\n    http api test demo\\n    doc: https://requests.readthedocs.io/en/master/\\n    "
    -        },
    -        "method": {
    -            "name": "test_get_method",
    -            "doc": "\\n        test get request\\n        "
    -        }
    -    },
    -    {
    -        "file": "module_api.test_http_demo",
    -        "class": {
    -            "name": "TestRequest",
    -            "doc": "\\n    http api test demo\\n    doc: https://requests.readthedocs.io/en/master/\\n    "
    -        },
    -        "method": {
    -            "name": "test_post_method",
    -            "doc": "\\n        test post request\\n        "
    -        }
    -    },
    -    {
    -        "file": "module_web.test_ddt_demo",
    -        "class": {
    -            "name": "BaiduTest",
    -            "doc": "Baidu search test case"
    -        },
    -        "method": {
    -            "name": "test_baidu_0",
    -            "doc": "used parameterized test [with name=1, search_key='seldom']\\n        :param name: case name\\n        :param search_key: search keyword\\n        "
    -        }
    -    },
    -    {
    -        "file": "module_web.test_ddt_demo",
    -        "class": {
    -            "name": "BaiduTest",
    -            "doc": "Baidu search test case"
    -        },
    -        "method": {
    -            "name": "test_baidu_1",
    -            "doc": "used parameterized test [with name=2, search_key='selenium']\\n        :param name: case name\\n        :param search_key: search keyword\\n        "
    -        }
    -    },
    -    {
    -        "file": "module_web.test_ddt_demo",
    -        "class": {
    -            "name": "BaiduTest",
    -            "doc": "Baidu search test case"
    -        },
    -        "method": {
    -            "name": "test_baidu_2",
    -            "doc": "used parameterized test [with name=3, search_key='unittest']\\n        :param name: case name\\n        :param search_key: search keyword\\n        "
    -        }
    -    },
    -    {
    -        "file": "module_web.test_first_demo",
    -        "class": {
    -            "name": "BaiduTest",
    -            "doc": "Baidu search test case"
    -        },
    -        "method": {
    -            "name": "test_case",
    -            "doc": "a simple test case "
    -        }
    -    }
    -]
    -

    数据结构说明:

    • file: 获取类的文件名,包含目录名。
    • class: 测试类的名字name 和 描述doc
    • method: 测试方法的名字name 和 描述doc

    执行用例信息

    当获取用例信息之后,可以进行自定义,例如 挑选出需要执行的用例,重新传给Seldom 执行。

    # run.py
    -from seldom import TestMainExtend
    -
    -if __name__ == '__main__':
    -    # 自定义要执行的用例
    -    cases = [
    -        {
    -            "file": "module_web.test_first_demo",
    -            "class": {
    -                "name": "BaiduTest",
    -                "doc": "Baidu search test case"
    -            },
    -            "method": {
    -                "name": "test_case",
    -                "doc": "a simple test case "
    -            }
    -        }
    -    ]
    -    main_extend = TestMainExtend(path="./test_dir")
    -    main_extend.run_cases(cases)
    -

    说明:

    • cases 定义要执行的用例信息, doc 非必填字段。
    • TestMainExtend(path="./test_dir") : 其中path指定从哪个目录查找用例集合。
    • run_cases(cases): 运行用例。

    相关项目

    seldom-platform: https://github.com/SeldomQA/seldom-platform

    `,19),p=[o];function c(i,l){return n(),a("div",null,p)}const r=s(e,[["render",c],["__file","platform.html.vue"]]);export{r as default}; diff --git a/assets/platform.html-f7529303.js b/assets/platform.html-c9aaf602.js similarity index 50% rename from assets/platform.html-f7529303.js rename to assets/platform.html-c9aaf602.js index db0e1d2..807ca01 100644 --- a/assets/platform.html-f7529303.js +++ b/assets/platform.html-c9aaf602.js @@ -1 +1 @@ -const e=JSON.parse('{"key":"v-3cdc5c3a","path":"/platform/platform.html","title":"平台化支持","lang":"en-US","frontmatter":{},"headers":[{"level":3,"title":"获取用例信息","slug":"获取用例信息","link":"#获取用例信息","children":[]},{"level":3,"title":"执行用例信息","slug":"执行用例信息","link":"#执行用例信息","children":[]},{"level":3,"title":"相关项目","slug":"相关项目","link":"#相关项目","children":[]}],"git":{"updatedTime":1667053189000,"contributors":[{"name":"defnngj","email":"defnngj@gmail.com","commits":1},{"name":"fnngj","email":"fnngj@126.com","commits":1},{"name":"yangqing","email":"yongchin39@qq.com","commits":1}]},"filePathRelative":"platform/platform.md"}');export{e as data}; +const e=JSON.parse('{"key":"v-3cdc5c3a","path":"/platform/platform.html","title":"平台化支持","lang":"en-US","frontmatter":{},"headers":[{"level":3,"title":"获取用例信息","slug":"获取用例信息","link":"#获取用例信息","children":[]},{"level":3,"title":"执行用例信息","slug":"执行用例信息","link":"#执行用例信息","children":[]},{"level":3,"title":"接入平台比读","slug":"接入平台比读","link":"#接入平台比读","children":[]}],"git":{"updatedTime":1734537706000,"contributors":[{"name":"defnngj","email":"defnngj@gmail.com","commits":3},{"name":"fnngj","email":"fnngj@126.com","commits":1},{"name":"yangqing","email":"yongchin39@qq.com","commits":1}]},"filePathRelative":"platform/platform.md"}');export{e as data}; diff --git a/assets/quick_start.html-87f1da62.js b/assets/quick_start.html-19c5f6ae.js similarity index 89% rename from assets/quick_start.html-87f1da62.js rename to assets/quick_start.html-19c5f6ae.js index 337a6e2..d448fe5 100644 --- a/assets/quick_start.html-87f1da62.js +++ b/assets/quick_start.html-19c5f6ae.js @@ -1 +1 @@ -const e=JSON.parse('{"key":"v-0f898c79","path":"/getting-started/quick_start.html","title":"快速开始","lang":"en-US","frontmatter":{},"headers":[{"level":3,"title":"基本规范","slug":"基本规范","link":"#基本规范","children":[]},{"level":3,"title":"main() 方法","slug":"main-方法","link":"#main-方法","children":[]},{"level":3,"title":"confrun.py 配置文件","slug":"confrun-py-配置文件","link":"#confrun-py-配置文件","children":[]},{"level":3,"title":"运行测试","slug":"运行测试","link":"#运行测试","children":[]},{"level":3,"title":"失败重跑","slug":"失败重跑","link":"#失败重跑","children":[]},{"level":3,"title":"测试报告","slug":"测试报告","link":"#测试报告","children":[]},{"level":3,"title":"多线程运行","slug":"多线程运行","link":"#多线程运行","children":[]}],"git":{"updatedTime":1725890482000,"contributors":[{"name":"defnngj","email":"defnngj@gmail.com","commits":19},{"name":"fnngj","email":"fnngj@126.com","commits":1},{"name":"yangqing","email":"yongchin39@qq.com","commits":1},{"name":"zhiheng","email":"zhiheng.hu@klook.com","commits":1},{"name":"虫师","email":"defnngj@gmail.com","commits":1}]},"filePathRelative":"getting-started/quick_start.md"}');export{e as data}; +const e=JSON.parse('{"key":"v-0f898c79","path":"/getting-started/quick_start.html","title":"快速开始","lang":"en-US","frontmatter":{},"headers":[{"level":3,"title":"基本规范","slug":"基本规范","link":"#基本规范","children":[]},{"level":3,"title":"main() 方法","slug":"main-方法","link":"#main-方法","children":[]},{"level":3,"title":"confrun.py 配置文件","slug":"confrun-py-配置文件","link":"#confrun-py-配置文件","children":[]},{"level":3,"title":"运行测试","slug":"运行测试","link":"#运行测试","children":[]},{"level":3,"title":"失败重跑","slug":"失败重跑","link":"#失败重跑","children":[]},{"level":3,"title":"测试报告","slug":"测试报告","link":"#测试报告","children":[]},{"level":3,"title":"多线程运行","slug":"多线程运行","link":"#多线程运行","children":[]}],"git":{"updatedTime":1734515068000,"contributors":[{"name":"defnngj","email":"defnngj@gmail.com","commits":21},{"name":"fnngj","email":"fnngj@126.com","commits":1},{"name":"yangqing","email":"yongchin39@qq.com","commits":1},{"name":"zhiheng","email":"zhiheng.hu@klook.com","commits":1},{"name":"虫师","email":"defnngj@gmail.com","commits":1}]},"filePathRelative":"getting-started/quick_start.md"}');export{e as data}; diff --git a/assets/quick_start.html-3d39b04b.js b/assets/quick_start.html-564351d1.js similarity index 94% rename from assets/quick_start.html-3d39b04b.js rename to assets/quick_start.html-564351d1.js index 5da7a70..c651e79 100644 --- a/assets/quick_start.html-3d39b04b.js +++ b/assets/quick_start.html-564351d1.js @@ -1,4 +1,4 @@ -import{_ as t,r as e,o as p,c as o,a as n,b as s,d as i,e as l}from"./app-a06a2d51.js";const c="/image/pycharm.png",u="/image/pycharm_run_case.png",d="/image/report.png",r={},k=n("h1",{id:"快速开始",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#快速开始","aria-hidden":"true"},"#"),s(" 快速开始")],-1),v=n("h3",{id:"基本规范",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#基本规范","aria-hidden":"true"},"#"),s(" 基本规范")],-1),m=n("code",null,"seldom",-1),b=n("code",null,"unittest",-1),q={href:"https://docs.python.org/3/library/unittest.html",target:"_blank",rel:"noopener noreferrer"},_=l(`
    # test_sample.py
    +import{_ as t,r as e,o as p,c as o,a as n,b as s,d as i,e as l}from"./app-9fb6f1b5.js";const c="/image/pycharm.png",u="/image/pycharm_run_case.png",d="/image/report.png",r={},k=n("h1",{id:"快速开始",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#快速开始","aria-hidden":"true"},"#"),s(" 快速开始")],-1),v=n("h3",{id:"基本规范",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#基本规范","aria-hidden":"true"},"#"),s(" 基本规范")],-1),m=n("code",null,"seldom",-1),b=n("code",null,"unittest",-1),q={href:"https://docs.python.org/3/library/unittest.html",target:"_blank",rel:"noopener noreferrer"},_=l(`
    # test_sample.py
     import seldom
     
     
    @@ -11,7 +11,7 @@ class YouTest(seldom.TestCaseif __name__ == '__main__':
         seldom.main()
    -

    基本规范:

    1. 创建测试类YouTest并继承seldom.TestCase类。
    2. 创建测试方法test_case, 必须以test开头。
    3. seldom.mian()是框架运行的入口方法,接下来详细介绍。

    main() 方法

    main()方法是seldom运行测试的入口, 它提供了一些最基本也是最重要的配置。

    import seldom
    +

    基本规范:

    1. 创建测试类YouTest并继承seldom.TestCase类。
    2. 创建测试方法test_case, 必须以test开头。
    3. seldom.main()是框架运行的入口方法,接下来详细介绍。

    main() 方法

    main()方法是seldom运行测试的入口, 它提供了一些最基本也是最重要的配置。

    import seldom
     
     # ...
     
    @@ -182,7 +182,7 @@ seldom confrun.py hooks function
         """Use case exe failed to stop, only support debug=True"""
         return False
     
    -

    以上配置根据需求自动化项目类型配置,相互可能冲突的钩子函数:

    • Web UI测试: browser()
    • http 接口测试: base_url()
    • app UI测试: app_info(), app_server()

    参数表格:

    seldom.main() (参数)confrun.py (函数)类型说明
    pathN/A通用指定测试目录或文件, 与case参数互斥。
    caseN/A通用指定测试用例, 与path参数互斥。
    browserbrowser()Web指定web测试运行的浏览器。
    base_urlbase_url()HTTP指定HTTP接口测试的基本URL。
    app_infoapp_info()Appapp 启动信息,参考appium desired_capabilities配置, app测试。
    app_serverapp_server()Appappium server 启动地址(默认 http://127.0.0.1:4723), app测试。
    reportreport()通用自定义测试报告的名称,例如result.html/result.xml
    titletitle()通用指定HTML报告标题。
    testertester()通用指定HTML报告测试人员。
    descriptiondescription()通用指定HTML报告描述。
    languagelanguage()通用设置HTML报告中英文,默认en, 中文zh-CN
    debugdebug()通用debug模式,设置为True不生成测试HTML测试,默认为False
    rerunrerun()通用设置失败重新运行次数。
    timeouttimeout()通用设置自动化全局超时时间,默认10秒。作用于元素定位、断言等。
    whitelistwhitelist()通用用例标签(label)设置白名单。
    blacklistblacklist()通用用例标签(label)设置黑名单。
    openN/A通用是否使用浏览器自动打开测试报告,默认True
    N/Amock_url()HTTP定义mock URL 映射。

    运行测试

    seldom 的运行有三种方式:

    • main() 方法:在.py 文件中使用seldom.main() 方法。
    • seldom 命令:通过sedom 命令指定要运行的目录&文件&类&方法。
    • pycharm右键执行:这种方式无法读取到配置,有严重缺陷。

    强烈建议使用前两种!!

    1. main()方法运行测试

    • 目录结构
    mypro/
    +

    以上配置根据需求自动化项目类型配置,相互可能冲突的钩子函数:

    • Web UI测试: browser()
    • http 接口测试: base_url()
    • app UI测试: app_info(), app_server()

    参数表格:

    seldom.main() (参数)confrun.py (函数)类型说明
    path-通用指定测试目录或文件, 与case参数互斥。
    case-通用指定测试用例, 与path参数互斥。
    browserbrowser()Web指定web测试运行的浏览器。
    base_urlbase_url()HTTP指定HTTP接口测试的基本URL。
    -mock_url()HTTP配置HTTP接口 mock URL。
    -proxies()HTTP配置HTTP接口proxies代理。
    app_infoapp_info()Appapp 启动信息,参考appium desired_capabilities配置, app测试。
    app_serverapp_server()Appappium server 启动地址(默认 http://127.0.0.1:4723), app测试。
    reportreport()通用自定义测试报告的名称,例如result.html/result.xml
    titletitle()通用指定HTML报告标题。
    testertester()通用指定HTML报告测试人员。
    descriptiondescription()通用指定HTML报告描述。
    languagelanguage()通用设置HTML报告中英文,默认en, 中文zh-CN
    debugdebug()通用debug模式,设置为True不生成测试HTML测试,默认为False
    rerunrerun()通用设置失败重新运行次数。
    timeouttimeout()通用设置自动化全局超时时间,默认10秒。作用于元素定位、断言等。
    whitelistwhitelist()通用用例标签(label)设置白名单。
    blacklistblacklist()通用用例标签(label)设置黑名单。
    open-通用是否使用浏览器自动打开测试报告,默认True

    运行测试

    seldom 的运行有三种方式:

    • main() 方法:在.py 文件中使用seldom.main() 方法。
    • seldom 命令:通过sedom 命令指定要运行的目录&文件&类&方法。
    • pycharm右键执行:这种方式无法读取到配置,有严重缺陷。

    强烈建议使用前两种!!

    1. main()方法运行测试

    • 目录结构
    mypro/
     ├── test_dir/
     │   ├── __init__.py
     │   ├── test_sample.py
    diff --git a/assets/quick_start.html-c208f012.js b/assets/quick_start.html-c208f012.js
    deleted file mode 100644
    index ac458e3..0000000
    --- a/assets/quick_start.html-c208f012.js
    +++ /dev/null
    @@ -1,358 +0,0 @@
    -import{_ as t,r as e,o as p,c as o,a as n,b as s,d as i,e as l}from"./app-72107ff1.js";const c="/image/pycharm.png",u="/image/pycharm_run_case.png",d="/image/report.png",r={},k=n("h1",{id:"快速开始",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#快速开始","aria-hidden":"true"},"#"),s(" 快速开始")],-1),v=n("h3",{id:"基本规范",tabindex:"-1"},[n("a",{class:"header-anchor",href:"#基本规范","aria-hidden":"true"},"#"),s(" 基本规范")],-1),m=n("code",null,"seldom",-1),b=n("code",null,"unittest",-1),q={href:"https://docs.python.org/3/library/unittest.html",target:"_blank",rel:"noopener noreferrer"},_=l(`
    # test_sample.py
    -import seldom
    -
    -
    -class YouTest(seldom.TestCase):
    -
    -    def test_case(self):
    -        """a simple test case """
    -        self.assertEqual(1+1, 2)
    -
    -
    -if __name__ == '__main__':
    -    seldom.main()
    -

    基本规范:

    1. 创建测试类YouTest并继承seldom.TestCase类。
    2. 创建测试方法test_case, 必须以test开头。
    3. seldom.mian()是框架运行的入口方法,接下来详细介绍。

    main() 方法

    main()方法是seldom运行测试的入口, 它提供了一些最基本也是最重要的配置。

    import seldom
    -
    -# ...
    -
    -if __name__ == '__main__':
    -    seldom.main(path="./",
    -                case="test_file.MyClassTest.test_case",
    -                browser="chrome",
    -                base_url=None,
    -                debug=False,
    -                timeout=10,
    -                app_server=None,
    -                app_info=None,
    -                report=None,
    -                title="百度测试用例",
    -                tester="虫师",
    -                description="测试环境:chrome",
    -                rerun=0,
    -                language="en",
    -                whitelist=[],
    -                blacklist=[],
    -                open=True,
    -                extensions=None,
    -                failfast=False
    -                )
    -

    参数说明

    • path : 指定测试目录或文件, 与case参数互斥。seldom > 3.7.0 支持 list 传多个目录或文件
    • case : 指定测试用例, 与path参数互斥。
    • browser : 指定浏览器("chrome"、"firefox" 等), Web测试。
    • base_url : 设置全局的基本URL, HTTP测试。
    • app_info : app 启动信息,参考desired_capabilities配置, app测试。
    • app_server : appium server 启动地址(默认 http://127.0.0.1:4723), app测试。
    • report : 自定义测试报告的名称,默认格式为2020_04_04_11_55_20_result.html
    • title : 指定测试报告标题。
    • tester : 指定测试人员, 默认Anonymous
    • description : 指定测试报告描述。
    • debug : debug模式,设置为True不生成测试HTML测试,默认为False
    • rerun : 设置失败重新运行次数,默认为 0
    • language : 设置HTML报告中英文,默认en, 中文zh-CN
    • timeout : 设置超时时间,默认10秒。
    • whitelist : 用例标签(label)设置白名单。
    • blacklist : 用例标签(label)设置黑名单。
    • open : 是否使用浏览器自动打开测试报告,默认True
    • extensions: 加载扩展,appium使用。
    • failfast: 当执行到失败的用例时,停止执行,仅在 debug=True时有效。

    confrun.py 配置文件

    seldom 3.1.0 提供过了 confrun.py 用于配置运行环境。 配置函数与 seldom.main() 的参数一致。

    在这个文件中可以定义运行相关的钩子函数。

    """
    -seldom confrun.py hooks function
    -"""
    -from seldom.appium_lab.android import UiAutomator2Options
    -
    -
    -def start_run():
    -    """
    -    Test the hook function before running
    -    """
    -    ...
    -
    -
    -def end_run():
    -    """
    -    Test the hook function after running
    -    """
    -    ...
    -
    -
    -def browser():
    -    """
    -    Web UI test:
    -    browser: gc(google chrome)/ff(firefox)/edge/ie/safari
    -    """
    -    return "gc"
    -
    -
    -def base_url():
    -    """
    -    http test
    -    api base url
    -    """
    -    return "http://httpbin.org"
    -
    -
    -def app_info():
    -    """
    -    app UI test
    -    appium app config
    -    """
    -    capabilities = {
    -        "automationName": "UiAutomator2",
    -        "platformName": "Android",
    -        "appPackage": "com.meizu.flyme.flymebbs",
    -        "appActivity": "com.meizu.myplus.ui.splash.SplashActivity",
    -        "noReset": True,
    -    }
    -    options = UiAutomator2Options().load_capabilities(capabilities)
    -    return options
    -
    -
    -def app_server():
    -    """
    -    app UI test
    -    appium server/desktop address
    -    """
    -    return "http://127.0.0.1:4723"
    -
    -
    -def debug():
    -    """
    -    debug mod
    -    """
    -    return False
    -
    -
    -def rerun():
    -    """
    -    error/failure rerun times
    -    """
    -    return 0
    -
    -
    -def report():
    -    """
    -    setting report path
    -    Used:
    -    return "d://mypro/result.html"
    -    return "d://mypro/result.xml"
    -    """
    -    return None
    -
    -
    -def timeout():
    -    """
    -    setting timeout
    -    """
    -    return 10
    -
    -
    -def title():
    -    """
    -    setting report title
    -    """
    -    return "seldom test report"
    -
    -
    -def tester():
    -    """
    -    setting report tester
    -    """
    -    return "bugmaster"
    -
    -
    -def description():
    -    """
    -    setting report description
    -    """
    -    return ["windows", "jenkins"]
    -
    -
    -def language():
    -    """
    -    setting report language
    -    return "en"
    -    return "zh-CN"
    -    """
    -    return "en"
    -
    -
    -def whitelist():
    -    """test label white list"""
    -    return []
    -
    -
    -def blacklist():
    -    """test label black list"""
    -    return []
    -
    -
    -def mock_url():
    -    """
    -    Replace the fixed url with the mock url
    -    :return:
    -    """
    -    config = {
    -        "http://httpbin.org/get": "http://127.0.0.1:8000/api/data",
    -    }
    -    return config
    -
    -
    -def failfast():
    -    """Use case exe failed to stop, only support debug=True"""
    -    return False
    -
    -

    以上配置根据需求自动化项目类型配置,相互可能冲突的钩子函数:

    • Web UI测试: browser()
    • http 接口测试: base_url()
    • app UI测试: app_info(), app_server()

    参数表格:

    seldom.main() (参数)confrun.py (函数)类型说明
    pathN/A通用指定测试目录或文件, 与case参数互斥。
    caseN/A通用指定测试用例, 与path参数互斥。
    browserbrowser()Web指定web测试运行的浏览器。
    base_urlbase_url()HTTP指定HTTP接口测试的基本URL。
    app_infoapp_info()Appapp 启动信息,参考appium desired_capabilities配置, app测试。
    app_serverapp_server()Appappium server 启动地址(默认 http://127.0.0.1:4723), app测试。
    reportreport()通用自定义测试报告的名称,例如result.html/result.xml
    titletitle()通用指定HTML报告标题。
    testertester()通用指定HTML报告测试人员。
    descriptiondescription()通用指定HTML报告描述。
    languagelanguage()通用设置HTML报告中英文,默认en, 中文zh-CN
    debugdebug()通用debug模式,设置为True不生成测试HTML测试,默认为False
    rerunrerun()通用设置失败重新运行次数。
    timeouttimeout()通用设置自动化全局超时时间,默认10秒。作用于元素定位、断言等。
    whitelistwhitelist()通用用例标签(label)设置白名单。
    blacklistblacklist()通用用例标签(label)设置黑名单。
    openN/A通用是否使用浏览器自动打开测试报告,默认True
    N/Amock_url()HTTP定义mock URL 映射。

    运行测试

    seldom 的运行有三种方式:

    • main() 方法:在.py 文件中使用seldom.main() 方法。
    • seldom 命令:通过sedom 命令指定要运行的目录&文件&类&方法。
    • pycharm右键执行:这种方式无法读取到配置,有严重缺陷。

    强烈建议使用前两种!!

    1. main()方法运行测试

    • 目录结构
    mypro/
    -├── test_dir/
    -│   ├── __init__.py
    -│   ├── test_sample.py
    -└── run.py  # 运行配置文件
    -

    创建 test_sample.py 文件,在测试文件中使用main()方法,如下:

    # test_sample.py
    -import seldom
    -from seldom import data
    -
    -
    -class YouTest(seldom.TestCase):
    -
    -    def test_case(self):
    -        """a simple test case """
    -        self.assertEqual(1 + 1, 2)
    -
    -    @data([
    -        ("case1", "seldom"),
    -        ("case2", "XTestRunner"),
    -    ])
    -    def test_ddt(self, name, search):
    -        """ ddt case """
    -        print(f"name: {name}, search_key: {search}")
    -
    -
    -if __name__ == '__main__':
    -    # 运行当前文件中的用例
    -    seldom.main()  # 默认运行当前文件中所有用例
    -    seldom.main(case="test_sample")  # 指定当前文件
    -    seldom.main(case="test_sample.YouTest")  # 指定测试类
    -    seldom.main(case="test_sample.YouTest.test_case")  # 指定测试用例
    -
    -    # 使用参数化的用例
    -    seldom.main(case="test_sample.YouTest.test_ddt")  # 错误用法
    -    seldom.main(case="test_sample.YouTest.test_ddt_0")  # 正确用法,0表示第一条数据用例
    -

    创建 run.py 文件,用于全局的指定要运行的用例。

    import seldom
    -
    -if __name__ == '__main__':
    -    # 指定运行其他目录&文件
    -    seldom.main(path="./")  # 指定当前文件所在目录下面的用例。
    -    seldom.main(path="./test_dir/")  # 指定当前目录下面的test_dir/ 目录下面的用例。
    -    seldom.main(path="./test_dir/test_sample.py")  # 指定测试文件中的用例。
    -    seldom.main(path="D:/seldom_sample/test_dir/test_sample.py")  # 指定文件的绝对路径。
    -
    -

    seldom.main() 提供哪些参数,请参考前面的文档。

    • 运行测试文件
    > cd mypro/  # 进入项目根目录
    -> python ./test_dir/test_sample.py      # 运行指定测试文件
    -> python run.py      # 运行run.py文件
    -

    2. seldom命令执行

    • 目录结构
    mypro/
    -├── test_dir/
    -│   ├── __init__.py
    -│   ├── test_sample.py
    -└── confrun.py  # 运行配置文件
    -

    seldom -p命令指定目录和文件。

    seldom -m命令可以提供更细粒度的运行。

    > cd mypro/  # 进入项目根目录
    -> seldom -p test_dir  # 运行目录
    -> seldom -p test_dir/test_sample.py  # 运行文件
    -> seldom -m test_dir.test_sample       # 运行文件
    -> seldom -m test_dir.test_sample.YouTest # 运行 SampleTest 测试类
    -> seldom -m test_dir.test_sample.YouTest.test_case # 运行 test_case 测试方法
    -

    运行相关的配置,可以在confrun.py 文件中配置。

    3. 在pyCharm中运行测试

    强烈不建议这种方式,除非你的测试用例没有任何依赖。

    步骤一:配置测试用例通过 unittest 运行。

    步骤二:在文件中选择测试类或用例执行。

    警告:运行用例打开的浏览器,需要手动关闭, seldom不做用例关闭操作。

    失败重跑

    Seldom支持错误&失败重跑。

    # test_sample.py
    -import seldom
    -
    -
    -class YouTest(seldom.TestCase):
    -
    -    def test_error(self):
    -        """error case"""
    -        self.assertEqual(a, 2)
    -
    -    def test_fail(self):
    -        """fail case """
    -        self.assertEqual(1 + 1, 3)
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(rerun=3)
    -

    参数说明:

    • rerun: 指定重跑的次数,默认为 0

    运行日志:

    > python test_sample.py
    -
    -
    -              __    __
    -   ________  / /___/ /___  ____ ____
    -  / ___/ _ \\/ / __  / __ \\/ __ \` ___/
    - (__  )  __/ / /_/ / /_/ / / / / / /
    -/____/\\___/_/\\__,_/\\____/_/ /_/ /_/  v3.x.x
    ------------------------------------------
    -                             @itest.info
    -
    -
    -
    -
    -XTestRunner Running tests...
    -
    -----------------------------------------------------------------------
    -ERetesting... test_error (test_sample.YouTest)..1
    -ERetesting... test_error (test_sample.YouTest)..2
    -ERetesting... test_error (test_sample.YouTest)..3
    -EFRetesting... test_fail (test_sample.YouTest)..1
    -FRetesting... test_fail (test_sample.YouTest)..2
    -FRetesting... test_fail (test_sample.YouTest)..3
    -Generating HTML reports...
    -F2022-07-12 00:22:52 log.py | SUCCESS | generated html file: file:///D:\\github\\seldom\\reports\\2022_07_12_00_22_51_result.html
    -2022-07-12 00:22:52 log.py | SUCCESS | generated log file: file:///D:\\github\\seldom\\reports\\seldom_log.log
    -

    测试报告

    seldom 默认生成HTML测试报告,在运行测试文件下自动创建reports目录。

    • 运行测试用例前
    mypro/
    -└── test_sample.py
    -
    • 运行测试用例后
    mypro/
    -├── reports/
    -│   ├── 2020_01_01_11_20_33_result.html
    -│   ├── seldom_log.log
    -└── test_sample.py
    -

    通过浏览器打开 2020_01_01_11_20_33_result.html 测试报告,查看测试结果。

    debug模式

    如果不想每次运行都生成HTML报告,可以打开debug模式。

    import seldom
    -
    -seldom.main(debug=True)
    -

    定义测试报告

    import seldom
    -
    -seldom.main(report="./report.html",
    -            title="百度测试用例",
    -            tester="虫师",
    -            description="测试环境:windows 10/ chrome")
    -
    • report: 配置报告名称和路径。
    • title: 自定义报告的标题。
    • tester: 指定自动化测试工程师名字。
    • description: 添加报告信息,支持列表, 例如:["OS: windows","Browser: chrome"]。

    XML测试报告

    如果需要生成XML格式的报告,只需要修改报告的后缀名为.xml即可。

    import seldom
    -
    -seldom.main(report="report.xml")
    -

    多线程运行

    多线程无疑可以缩短用例的运行时间,一般由两种方式实现。

    1. 设置线程数,交由框架去分配用例,或按照测试用例、测试类、测试模块分配给线程执行。
    • 优点:简单,例如 pytest-xdist ,只需要指定 线程数 即可。
    • 缺点:无法控制用例的拆分粒度,如果在设计用例时,不同的用例有依赖,刚好被分到的不同的线程,那么必定导致用例失败。
    1. 自己分好线程,分别调用框架执行。
    • 优点:手动划分线程,可以按照目录、文件、甚至测试类或方法 拆分线程。
    • 缺点:首先会比较麻烦,而且多个线程的执行结果无法很好的合并到一起。

    seldom 推荐第二种方法,把线程的划分方式交给用户,无疑是更灵活的方法。至于报告的合并统计就每有什么好办法了。

    • 用例维度使用多线程。
    import seldom
    -from seldom.extend_lib import threads
    -
    -
    -class MyTest(seldom.TestCase):
    -
    -    def test_baidu(self):
    -        self.open("https://www.baidu.com")
    -        self.sleep(3)
    -
    -    def test_bing(self):
    -        self.open("https://www.bing.com")
    -        self.sleep(4)
    -
    -
    -@threads(2)  # !!!核心!!!! 设置线程数
    -def run_case(case: str, browser: str):
    -    """
    -    根据传入的case执行用例
    -    """
    -    seldom.main(case=case, browser=browser, debug=True)
    -
    -
    -if __name__ == "__main__":
    -    # 将两条用例拆分,分别用不同的浏览器执行
    -    cases = {
    -        "test_thread_case.MyTest.test_baidu": "chrome",
    -        "test_thread_case.MyTest.test_bing": "edge"
    -    }
    -
    -    for key, value in cases.items():
    -        run_case(key, value)
    -
    • 目录或文件维度使用多线程。
    import seldom
    -from seldom.extend_lib import threads
    -
    -
    -@threads(3)  # !!!核心!!!! 设置线程数
    -def run_case(path: str):
    -    """
    -    根据传入的path执行用例
    -    """
    -    seldom.main(path=path, debug=True)
    -
    -
    -if __name__ == "__main__":
    -    # 定义3个测试文件,分别丢给3个线程执行。
    -    paths = [
    -        "./test_dir/more_case/test_case1.py",
    -        "./test_dir/more_case/test_case2.py",
    -        "./test_dir/more_case/test_case3.py"
    -    ]
    -    for p in paths:
    -        run_case(p)
    -
    `,79);function g(h,y){const a=e("ExternalLinkIcon");return p(),o("div",null,[k,v,n("p",null,[m,s("继承"),b,s("单元测试框架,所以他的编写规范与"),n("a",q,[s("unittest"),i(a)]),s("基本保持一致。")]),_])}const w=t(r,[["render",g],["__file","quick_start.html.vue"]]);export{w as default}; diff --git a/assets/seldom_api.html-a9e2c335.js b/assets/seldom_api.html-a9e2c335.js deleted file mode 100644 index 1cc77f7..0000000 --- a/assets/seldom_api.html-a9e2c335.js +++ /dev/null @@ -1,274 +0,0 @@ -import{_ as p,r as o,o as i,c,a as n,b as a,d as t,e}from"./app-72107ff1.js";const l={},u=e(`

    Seldom API

    查找元素

    seldom 提供了8中定位方式,与Selenium保持一致。

    • id_
    • name
    • class_name
    • tag
    • link_text
    • partial_link_text
    • css
    • xpath

    使用方式

    # **kwargs
    -self.type(id_="kw", text="seldom")
    -self.type(name="wd", text="seldom")
    -self.type(class_name="s_ipt", text="seldom")
    -self.type(tag="input", text="seldom")
    -self.type(xpath="//input[@id='kw']", text="seldom")
    -self.type(css="#kw", text="seldom")
    -self.click(link_text="hao123")
    -self.click(partial_link_text="hao")
    -
    -# selector
    -self.type("id=kw", text="seldom")
    -self.type("name=wd", text="seldom")
    -self.type("class=s_ipt", text="seldom")
    -self.type("tag=input", text="seldom")
    -self.type("//input[@id='kw']", text="seldom")  # xpath
    -self.type("#kw", text="seldom")  # css
    -self.click("text=hao123")
    -self.click("text*=hao")
    -

    seldom 3.10.0 引入新的 selector 定位,弱化了selenium/appium 的定位类型方式。

    • **kwargsselector 定位对比。
    类型定位**kwargsselector
    selenium/appiumidid_="id""id=id"
    seleniummamename="name""name=name"
    selenium/appiumclassclass_name="class""class=class"
    seleniumtagtag="input""tag=input"
    seleniumlink_textlink_text="文字链接""text=文字链接"
    seleniumpartial_link_textpartial_link_text="文字链""text~=文字链"
    selenium/appiumxpathxpath="//*[@id='11']""//*[@id='11']"
    seleniumcsscas="input#id""input#id"
    appiumios_predicateios_predicate = "xx""ios_predicate=xx"
    appiumios_class_chainios_class_chain = "xx""ios_predicate=xx"
    appiumandroid_uiautomatorandroid_uiautomator = "xx""android_uiautomator=xx"
    appiumandroid_viewtagandroid_viewtag = "xx""android_viewtag=xx"
    appiumandroid_data_matcherandroid_data_matcher = "xx""android_data_matcher=xx"
    appiumandroid_view_matcherandroid_view_matcher = "xx""android_view_matcher=xx"
    appiumaccessibility_idaccessibility_id = "xx""accessibility_id=xx"
    appiumimageimage = "xx""image=xx"
    appiumcustomcustom = "xx""custom=xx"

    帮助信息

    `,10),d={href:"https://www.w3school.com.cn/cssref/css_selectors.asp",target:"_blank",rel:"noopener noreferrer"},r={href:"https://www.w3school.com.cn/xpath/xpath_syntax.asp",target:"_blank",rel:"noopener noreferrer"},k=e(`

    使用下标

    有时候无法通过一种定位找到单个元素,那么可以通过index指定一组元素中的第几个。

    self.type(tag="input", index=7, text="seldom")
    -

    通过tag="input"匹配出一组元素, index=7 指定这一组元素中的第8个,index默认下标为0

    断言

    seldom 提供了一组针对Web页面的断言方法。

    使用方法

    # 断言标题是否等于"title"
    -self.assertTitle("title")
    -
    -# 断言标题是否包含"title"
    -self.assertInTitle("title")
    -
    -# 断言URL是否等于
    -self.assertUrl("url")
    -
    -# 断言URL是否包含
    -self.assertInUrl("url")
    -
    -# 断言页面包含“text”
    -self.assertText("text")
    -
    -# 断言页面不包含“text”
    -self.assertNotText("text")
    -
    -# 断言警告是否存在"text" 提示信息
    -self.assertAlertText("text")
    -
    -# 断言元素是否存在
    -self.assertElement(css="#kw")
    -
    -# 断言元素是否不存在
    -self.assertNotElement(css="#kwasdfasdfa")
    -

    WebDriver API

    seldom简化了selenium中的API,使操作Web页面更加简单。

    大部分API都由WebDriver类提供:

    import seldom
    -
    -
    -class TestCase(seldom.TestCase):
    -
    -    def test_seldom_api(self):
    -        # Accept warning box. ->  Be removed in the future
    -        self.accept_alert()
    -
    -        # Adds a cookie to your current session.
    -        self.add_cookie({'name': 'foo', 'value': 'bar'})
    -
    -        # Adds a cookie to your current session.
    -        cookie_list = [
    -            {'name': 'foo', 'value': 'bar'},
    -            {'name': 'foo', 'value': 'bar'}
    -        ]
    -        self.add_cookies(cookie_list)
    -
    -        # Clear the contents of the input box.
    -        self.clear(css="#el")
    -
    -        # It can click any text / image can be clicked
    -        # Connection, check box, radio buttons, and even drop-down box etc..
    -        self.click(css="#el")
    -
    -        # Mouse over the element.
    -        self.move_to_element(css="#el")
    -
    -        # Click the element by the link text
    -        self.click_text("新闻")
    -
    -        # Simulates the user clicking the "close" button in the titlebar of a popup window or tab.
    -        self.close()
    -
    -        # Delete all cookies in the scope of the session.
    -        self.delete_all_cookies()
    -
    -        # Deletes a single cookie with the given name.
    -        self.delete_cookie('my_cookie')
    -
    -        # Dismisses the alert available.  ->  Be removed in the future
    -        self.dismiss_alert()
    -
    -        # Double click element.
    -        self.double_click(css="#el")
    -
    -        # Execute JavaScript scripts.
    -        self.execute_script("window.scrollTo(200,1000);")
    -
    -        # Setting width and height of window scroll bar.
    -        self.window_scroll(width=300, height=500)
    -
    -        # Setting width and height of element scroll bar.
    -        self.element_scroll(css=".class", width=300, height=500)
    -
    -        # open url.
    -        self.open("https://www.baidu.com")
    -
    -        # Gets the text of the Alert.  ->  Be removed in the future
    -        alert_title = self.get_alert_text
    -
    -        # Execute Chrome Devtools Protocol command and get returned result 
    -        self.execute_cdp_cmd('Runtime.evaluate', {'expression': "alert('hello world')"})
    -
    -        # Gets the value of an element attribute.
    -        self.get_attribute(css="#el", attribute="type")
    -
    -        # Returns information of cookie with \`\`name\`\` as an object.
    -        self.get_cookie(name="kkk")
    -
    -        # Returns a set of dictionaries, corresponding to cookies visible in the current session.
    -        self.get_cookies()
    -
    -        # Gets the element to display,The return result is true or false.
    -        self.get_display(css="#el")
    -
    -        # Get a set of elements
    -        self.get_element(css="#el", index=0)
    -
    -        # Get element text information.
    -        self.get_text(css="#el")
    -
    -        # Get window title.
    -        title = self.get_title
    -
    -        # Get the URL address of the current page.
    -        url = self.get_url
    -
    -        # Gets the log for a given log type
    -        logs = self.get_log("browser")
    -
    -        # Set browser window maximized.
    -        self.max_window()
    -
    -        # open url.
    -        self.open("https://www.baidu.com")
    -
    -        # Quit the driver and close all the windows.
    -        self.quit()
    -
    -        # Refresh the current page.
    -        self.refresh()
    -
    -        # Right click element.
    -        self.right_click(css="#el")
    -
    -        # Saves a screenshots of the current window to a PNG image file.
    -        self.screenshots()  # Save to HTML report
    -        self.screenshots('/Screenshots/foo.png')  # Save to the specified directory
    -
    -        # Saves a element screenshot of the element to a PNG image file.
    -        self.element_screenshot(css="#id")  # Save to HTML report
    -        self.element_screenshot(css="#id", file_path='/Screenshots/foo.png')  # Save to the specified directory
    -
    -        """
    -        Constructor. A check is made that the given element is, indeed, a SELECT tag. If it is not,
    -        then an UnexpectedTagNameException is thrown.
    -        <select name="NR" id="nr">
    -            <option value="10" selected="">每页显示10条</option>
    -            <option value="20">每页显示20条</option>
    -            <option value="50">每页显示50条</option>
    -        </select>
    -        """
    -        self.select(css="#nr", value='20')
    -        self.select(css="#nr", text='每页显示20条')
    -        self.select(css="#nr", index=2)
    -
    -        # Set browser window wide and high.
    -        self.set_window(100, 200)
    -
    -        # Submit the specified form.
    -        self.submit(css="#el")
    -
    -        # Switch to the specified frame.
    -        self.switch_to_frame(css="#el")
    -
    -        # Switches focus to the parent context. If the current context is the top
    -        # level browsing context, the context remains unchanged.
    -        self.switch_to_frame_parent()
    -
    -        # Returns the current form machine form at the next higher level.
    -        # Corresponding relationship with switch_to_frame () method.
    -        self.switch_to_frame_out()
    -
    -        # Switches focus to the specified window.
    -        # This switches to the new windows/tab (0 is the first one)
    -        self.switch_to_window(1)
    -
    -        # Operation input box.
    -        self.type(css="#el", text="selenium")
    -
    -        # Implicitly wait.All elements on the page.
    -        self.wait(10)
    -
    -        # Setting width and height of window scroll bar.
    -        self.window_scroll(width=300, height=500)
    -
    -        # alert operation. (seldom>=3.2.0)
    -        text = self.alert.text
    -        self.alert.accept()
    -        self.alert.dismiss()
    -        self.alert.send_keys("text")
    -
    -

    键盘操作

    有时候我们需要用到键盘操作,比如EnterBackspaceTAB,或者ctrl/command + actrl/command + c组合键操作,seldom提供了一组键盘操作。

    使用方法

    import seldom
    -
    -
    -class Test(seldom.TestCase):
    -
    -    def test_key(self):
    -        self.open("https://www.baidu.com")
    -
    -        # 输入 seldomm
    -        self.Keys(css="#kw").input("seldomm")
    -
    -        # 删除多输入的一个m
    -        self.Keys(id_="kw").backspace()
    -
    -        # 输入“教程”
    -        self.Keys(id_="kw").input("教程")
    -
    -        # ctrl+a 全选输入框内容
    -        self.Keys(id_="kw").select_all()
    -
    -        # ctrl+x 剪切输入框内容
    -        self.Keys(id_="kw").cut()
    -
    -        # ctrl+v 粘贴内容到输入框
    -        self.Keys(id_="kw").paste()
    -
    -        # 通过回车键来代替单击操作
    -        self.Keys(id_="kw").enter()
    -
    -        # 支持组合操作
    -        self.Keys(id_="kw").select_all().cut()  # 全选剪切
    -        self.Keys(id_="kw").select_all().delete()  # 全选删除
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(browser="firefox", debug=True)
    -
    -

    测试electron应用

    Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js 到 二进制的 Electron 允许您保持一个 JavaScript代码代码库并创建 在Windows、macOS和Linux上运行的跨平台应用。

    https://www.electronjs.org/

    import seldom
    -
    -
    -class DataDriverTest(seldom.TestCase):
    -
    -    def start(self):
    -        # appium-desktop基于Electron开发的桌面应用
    -        self.app_path = f"C:\\Program Files\\Appium Server GUI\\Appium Server GUI.exe"
    -
    -    def test_case(self):
    -        """
    -        Used tuple test data
    -        """
    -        self.open_electron(app_path=self.app_path)
    -        self.sleep(10)
    -        self.switch_to_window(0)
    -        self.Keys(css="#simpleHostInput").select_all().delete()
    -        self.type(css="#simpleHostInput", text="127.0.0.1")
    -        self.Keys(css="#simplePortInput").select_all().delete()
    -        self.type(css="#simplePortInput", text="4724")
    -        self.click(css="#startServerBtn")
    -        self.sleep(5)
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(debug=True)
    -
    `,20);function m(v,b){const s=o("ExternalLinkIcon");return i(),c("div",null,[u,n("ul",null,[n("li",null,[n("a",d,[a("CSS选择器"),t(s)])]),n("li",null,[n("a",r,[a("xpath语法"),t(s)])])]),k])}const g=p(l,[["render",m],["__file","seldom_api.html.vue"]]);export{g as default}; diff --git a/assets/seldom_api.html-a8ef0a30.js b/assets/seldom_api.html-e8682244.js similarity index 99% rename from assets/seldom_api.html-a8ef0a30.js rename to assets/seldom_api.html-e8682244.js index ce274f7..082a2db 100644 --- a/assets/seldom_api.html-a8ef0a30.js +++ b/assets/seldom_api.html-e8682244.js @@ -1,4 +1,4 @@ -import{_ as p,r as o,o as i,c,a as n,b as a,d as t,e}from"./app-a06a2d51.js";const l={},u=e(`

    Seldom API

    查找元素

    seldom 提供了8中定位方式,与Selenium保持一致。

    • id_
    • name
    • class_name
    • tag
    • link_text
    • partial_link_text
    • css
    • xpath

    使用方式

    # **kwargs
    +import{_ as p,r as o,o as i,c,a as n,b as a,d as t,e}from"./app-9fb6f1b5.js";const l={},u=e(`

    Seldom API

    查找元素

    seldom 提供了8中定位方式,与Selenium保持一致。

    • id_
    • name
    • class_name
    • tag
    • link_text
    • partial_link_text
    • css
    • xpath

    使用方式

    # **kwargs
     self.type(id_="kw", text="seldom")
     self.type(name="wd", text="seldom")
     self.type(class_name="s_ipt", text="seldom")
    diff --git a/assets/seldom_cli.html-3837c5f4.js b/assets/seldom_cli.html-29118a10.js
    similarity index 99%
    rename from assets/seldom_cli.html-3837c5f4.js
    rename to assets/seldom_cli.html-29118a10.js
    index b66961d..786e6f0 100644
    --- a/assets/seldom_cli.html-3837c5f4.js
    +++ b/assets/seldom_cli.html-29118a10.js
    @@ -1,4 +1,4 @@
    -import{_ as a,o as s,c as e,e as n}from"./app-a06a2d51.js";const l={},t=n(`

    seldom CLI

    seldom 2.10.7 对命令行工具做了增强,可以使用命令行的方式运行用例。

    seldom 帮助

    • seldom --help 查看帮助使用
    > seldom --help
    +import{_ as a,o as s,c as e,e as n}from"./app-9fb6f1b5.js";const l={},t=n(`

    seldom CLI

    seldom 2.10.7 对命令行工具做了增强,可以使用命令行的方式运行用例。

    seldom 帮助

    • seldom --help 查看帮助使用
    > seldom --help
     Usage: seldom [OPTIONS]
     
       seldom CLI.
    diff --git a/assets/seldom_cli.html-e3f8243b.js b/assets/seldom_cli.html-e3f8243b.js
    deleted file mode 100644
    index 4e50608..0000000
    --- a/assets/seldom_cli.html-e3f8243b.js
    +++ /dev/null
    @@ -1,86 +0,0 @@
    -import{_ as a,o as s,c as e,e as n}from"./app-72107ff1.js";const l={},t=n(`

    seldom CLI

    seldom 2.10.7 对命令行工具做了增强,可以使用命令行的方式运行用例。

    seldom 帮助

    • seldom --help 查看帮助使用
    > seldom --help
    -Usage: seldom [OPTIONS]
    -
    -  seldom CLI.
    -
    -Options:
    -  --version                       Show version.
    -  --project-api TEXT              Create an API automation test project.
    -  --project-app TEXT              Create an App automation test project.
    -  --project-web TEXT              Create an Web automation test project.
    -  -cc, --clear-cache BOOLEAN      Clear all caches of seldom.
    -  -p, --path TEXT                 Run test case file path.
    -  -c, --collect BOOLEAN           Collect project test cases. Need the
    -                                  \`--path\`.
    -  -l, --level [data|method]       Parse the level of use cases. Need the
    -                                  --path.
    -  -j, --case-json TEXT            Test case files. Need the \`--path\`.
    -  -e, --env TEXT                  Set the Seldom run environment \`Seldom.env\`.
    -  -b, --browser [chrome|firefox|ie|edge|safari]
    -                                  The browser that runs the Web UI automation
    -                                  tests. Need the \`--path\`.
    -  -u, --base-url TEXT             The base-url that runs the HTTP automation
    -                                  tests. Need the \`--path\`.
    -  -d, --debug BOOLEAN             Debug mode. Need the \`--path\`.
    -  -rr, --rerun INTEGER            The number of times a use case failed to run
    -                                  again. Need the \`--path\`.
    -  -r, --report TEXT               Set the test report for output. Need the
    -                                  \`--path\`.
    -  -m, --mod TEXT                  Run tests modules, classes or even
    -                                  individual test methods from the command
    -                                  line.
    -  -ll, --log-level [TRACE|DEBUG|INFO|SUCCESS|WARNING|ERROR]
    -                                  Set the log level.
    -  -h2c, --har2case TEXT           HAR file converts an seldom test case.
    -  -s2c, --swagger2case TEXT       Swagger file converts an seldom test case.
    -  --api-excel TEXT                Run the api test cases in the excel file.
    -  --help                          Show this message and exit.
    -

    如果无法使用seldom 命令。

    1. 请确保你已经安装了seldom
    > pip install seldom
    -
    1. 如果仍然无法使用seldom命令,请用where检查安装位置。
    > where seldom
    -C:\\Python311\\Scripts\\seldom.exe
    -

    seldom 使用

    创建项目

    • -P/--project
    > seldom -P mypro
    -2022-09-03 11:22:25 cli.py | INFO | Start to create new test project: mypro
    -2022-09-03 11:22:25 cli.py | INFO | CWD: D:\\github\\seldom
    -
    -2022-09-03 11:22:25 cli.py | INFO | created folder: mypro
    -2022-09-03 11:22:25 cli.py | INFO | created folder: mypro\\test_dir
    -2022-09-03 11:22:25 cli.py | INFO | created folder: mypro\\reports
    -2022-09-03 11:22:25 cli.py | INFO | created folder: mypro\\test_data
    -2022-09-03 11:22:25 cli.py | INFO | created file: mypro\\test_data\\data.json
    -2022-09-03 11:22:25 cli.py | INFO | created file: mypro\\test_dir\\test_web_sample.py
    -2022-09-03 11:22:25 cli.py | INFO | created file: mypro\\test_dir\\test_api_sample.py
    -2022-09-03 11:22:25 cli.py | INFO | created file: mypro\\run.py
    -

    生成接口自动化用例

    • -h2c/--har2case
    > seldom -h2c demo.har
    -2022-09-03 11:29:29 core.py | INFO | demo.py
    -2022-09-03 11:29:29 core.py | INFO | Start to generate testcase.
    -2022-09-03 11:29:29 core.py | INFO | created file: D:\\github\\seldom\\seldom\\har2case\\demo.py
    -

    注:har 是fiddler 抓包工具导出的一种格式,即 HTTPArchive

    运行测试目录&文件

    • -p\\--path
    > seldom -p ./test_dir/                     # 指定运行目录
    -> seldom -p ./test_dir/test_first_demo.py   # 指定运行文件
    -

    不支持斜杠\\表示路径

    运行文件&类&方法

    • -m\\--mod
    > seldom -m test_first_demo            # 文件名,不要.py后缀
    -> seldom -m test_first_demo.BingTest   # 文件名.类名
    -> seldom -m test_first_demo.BingTest.test_case  # 文件名.类名.方法名
    -

    调试模式

    • -d, --debug/ -nd, --no-debug
    > seldom -p test_first_demo.py -d   # 开启debug模式
    -> seldom -p test_first_demo.py -nd   # 关闭debug模式
    -

    运行浏览器

    • -b/--browser
    > seldom -p test_first_demo.py -b firefox  # firefox浏览器
    -

    支持[chrome|firefox|ie|edge] 浏览器。

    运行URL

    • -u/--base-url
    > seldom -p test_http_demo.py -u http://httpbin.org  # base-url
    -

    测试报告

    • -r/--report
    > seldom -p test_first_demo.py -r result.html  # HTML报告
    -> seldom -p test_first_demo.py -r result.xml  # XML报告
    -

    失败/错误重跑次数

    • -rr/--rerun
    > seldom -p test_first_demo.py -rr 2  # rerun重跑次数
    -

    数据驱动运行环境

    • -e/--env
    > seldom -p test_ddt_demo.py -e production  # 运行环境
    -

    注:参考数据驱动 一章 Seldom.env 的用法。

    收集测试用例

    > seldom -p test_dir -c -l method -j case.json
    -Collect use cases for the test_dir directory.
    -add env Path: .
    -
    -              __    __
    -   ________  / /___/ /___  ____ ____
    -  / ___/ _ \\/ / __  / __ \\/ __ \` ___/
    - (__  )  __/ / /_/ / /_/ / / / / / /
    -/____/\\___/_/\\__,_/\\____/_/ /_/ /_/  v{x}.{x}.{x}
    ------------------------------------------
    -                             @itest.info
    -
    -save them to D:\\github\\seldom\\demo\\case.json
    -
    • 说明:
      • -p/--path: 指定收集用例的目录:test_dir
      • -c, --collect / -nc, --no-collect: 是否收集用例, 默认false
      • -l/--level: 是否收集用例级别: data/method
      • -j/--case-json: 收集用例保存文件: case.json

    运行收集测试用例

    > seldom -p test_dir -j case.json -r result.html
    -
    • 说明:
      • -p/--path: 指定运行用例的根目录:test_dir
      • -j/--case-json: 运行收集用例文件: case.json
      • -r/--report: 运行收集用例生成报告: result.html

    清除所有缓存

    > seldom --clear-cache true
    -
    • 说明:默认清空seldom所有缓存,即cache.clear()

    执行 API(excel文件)测试用例

    > seldom  --api-excel api_case.xlsx
    -
    • 说明:简单的HTTP接口测试可以使用excel编写,seldom支持运行excel文件。excel的具体定义可以参考HTTP接口测试章节。
    `,57),p=[t];function o(i,r){return s(),e("div",null,p)}const d=a(l,[["render",o],["__file","seldom_cli.html.vue"]]);export{d as default}; diff --git a/assets/start.html-39c759f1.js b/assets/start.html-6c306c3b.js similarity index 99% rename from assets/start.html-39c759f1.js rename to assets/start.html-6c306c3b.js index 5465ec4..e862da1 100644 --- a/assets/start.html-39c759f1.js +++ b/assets/start.html-6c306c3b.js @@ -1,4 +1,4 @@ -import{_ as e,r as p,o,c as l,a as n,b as s,d as i,e as a}from"./app-a06a2d51.js";const c={},u=a('

    开始使用

    前言

    seldom 非常适合个人接口自动化项目,它有以下优势。

    • 可以写更少的代码
    • 提供详细的运行日志
    • 提供专门为接口设计的断言
    • 强大的数据驱动
    • 自动生成HTML/XML测试报告
    • 支持生成随机数据
    • 支持har/swagger文件转case
    • 支持数据库操作

    这些是seldom支持的功能,我们只需要集成HTTP接口库,并提供强大的断言即可。seldom 2.0 加入了HTTP接口自动化测试支持。

    ',5),r={href:"https://docs.python-requests.org/en/master/",target:"_blank",rel:"noopener noreferrer"},d=a(`
    seldomrequests
    self.get()requests.get()
    self.post()requests.post()
    self.put()requests.put()
    self.delete()requests.delete()
    self.patch()requests.patch()
    self.session()requests.session()

    Seldom VS Request+unittest

    • unittest + requests 接口自动化示例:
    import unittest
    +import{_ as e,r as p,o,c as l,a as n,b as s,d as i,e as a}from"./app-9fb6f1b5.js";const c={},u=a('

    开始使用

    前言

    seldom 非常适合个人接口自动化项目,它有以下优势。

    • 可以写更少的代码
    • 提供详细的运行日志
    • 提供专门为接口设计的断言
    • 强大的数据驱动
    • 自动生成HTML/XML测试报告
    • 支持生成随机数据
    • 支持har/swagger文件转case
    • 支持数据库操作

    这些是seldom支持的功能,我们只需要集成HTTP接口库,并提供强大的断言即可。seldom 2.0 加入了HTTP接口自动化测试支持。

    ',5),r={href:"https://docs.python-requests.org/en/master/",target:"_blank",rel:"noopener noreferrer"},d=a(`
    seldomrequests
    self.get()requests.get()
    self.post()requests.post()
    self.put()requests.put()
    self.delete()requests.delete()
    self.patch()requests.patch()
    self.session()requests.session()

    Seldom VS Request+unittest

    • unittest + requests 接口自动化示例:
    import unittest
     import requests
     
     
    diff --git a/assets/start.html-75c073f4.js b/assets/start.html-75c073f4.js
    deleted file mode 100644
    index b3d6191..0000000
    --- a/assets/start.html-75c073f4.js
    +++ /dev/null
    @@ -1,79 +0,0 @@
    -import{_ as n,o as s,c as a,e}from"./app-a06a2d51.js";const p={},t=e(`

    app 测试

    seldom 3.0 基于appium支持APP测试。

    appium 官方网站:https://appium.io/

    环境安装

    app 的自动化测试环境相比较 web 要复杂一些,请参考appium官方。

    1. 安装node

    https://nodejs.org/en/

    > node --version
    -v16.17.0
    -
    1. 安装appium
    > npm i --location=global appium  # appium 2.x
    -
    1. 启动appium
    > appium server --address '127.0.0.1' -p 4723
    -
    -[Appium] Welcome to Appium v2.2.2
    -[Appium] Non-default server args:
    -[Appium] {
    -[Appium]   address: '127.0.0.1'
    -[Appium] }
    -...
    -
    1. 移动设备

    准备一台设备(Android/iOS手机)通过USB数据线连接电脑。通过以下工具确认手机与电脑是否连接。

    • adb
    > adb devices 
    -List of devices attached
    -UMXDU000000000000       device
    -
    • taobao-iphone-device
    > tidevice list
    -List of apple devices attached
    -00008030-00000000000000 xxx的iPhoneSE
    -

    编写测试

    基于seldom编写app自动化测试, 由于appium 继承自selenium,所以,部分API共用。

    import seldom
    -from seldom.appium_lab.android import UiAutomator2Options
    -
    -
    -class TestBBS(seldom.TestCase):
    -
    -    def test_bbs_search(self):
    -        self.click(id_="com.meizu.flyme.flymebbs:id/nw")
    -        self.type(id_="com.meizu.flyme.flymebbs:id/nw", text="flyme")
    -        self.click(id_="com.meizu.flyme.flymebbs:id/o1")
    -        self.sleep(2)
    -        elems = self.get_elements(id_="com.meizu.flyme.flymebbs:id/a29")
    -        for title in elems:
    -            print(title.text)
    -            self.assertIn("lyme", title.text)
    -
    -
    -if __name__ == '__main__':
    -    capabilities = {
    -        "automationName": "UiAutomator2",
    -        "platformName": "Android",
    -        "appPackage": "com.meizu.flyme.flymebbs",
    -        "appActivity": "com.meizu.myplus.ui.splash.SplashActivity",
    -        "noReset": True,
    -    }
    -    options = UiAutomator2Options().load_capabilities(capabilities)
    -    seldom.main(app_server="http://127.0.0.1:4723", app_info=options)
    -

    注:上面的测试用例隐含了appium的一些知识点,你需要对appium有足够的了解。

    • 运行日志
    python test_app.py
    -
    -              __    __
    -   ________  / /___/ /___  ____ ____
    -  / ___/ _ \\/ / __  / __ \\/ __ \` ___/
    - (__  )  __/ / /_/ / /_/ / / / / / /
    -/____/\\___/_/\\__,_/\\____/_/ /_/ /_/  v3.0.0
    ------------------------------------------
    -                             @itest.info
    -
    -
    -XTestRunner Running tests...
    -
    -----------------------------------------------------------------------
    -2022-10-03 00:01:30 webdriver.py | INFO | 💤️ sleep: 5s.
    -2022-10-03 00:01:35 webdriver.py | INFO | ✅ Find 1 element: id=com.meizu.flyme.flymebbs:id/nw  -> click.
    -2022-10-03 00:01:36 webdriver.py | INFO | ✅ Find 1 element: id=com.meizu.flyme.flymebbs:id/nw  -> input 'flyme'.
    -2022-10-03 00:01:37 webdriver.py | INFO | ✅ Find 1 element: id=com.meizu.flyme.flymebbs:id/o1  -> click.
    -2022-10-03 00:01:37 webdriver.py | INFO | 💤️ sleep: 2s.
    -2022-10-03 00:01:39 webdriver.py | INFO | ✅ Find 5 element: id=com.meizu.flyme.flymebbs:id/a29 .
    -flyme的屏幕色彩显示应该是比较差的
    -
    -魅族17的Flyme9状态栏下拉问题。
    -
    -flyme9.3连上耳机来电话还是会外放
    -
    -flyme自带录屏功能吗?
    -
    -关于Flyme 8.18.0A稳定版
    -
    -
    -Generating HTML reports...
    -.12022-10-03 00:01:40 runner.py | SUCCESS | generated html file: file:///D:\\github\\seldom\\reports\\2022_10_03_00_01_23_result.html
    -2022-10-03 00:01:40 runner.py | SUCCESS | generated log file: file:///D:\\github\\seldom\\reports\\seldom_log.log
    -
    `,24),i=[t];function l(o,c){return s(),a("div",null,i)}const r=n(p,[["render",l],["__file","start.html.vue"]]);export{r as default}; diff --git a/assets/start.html-a4c1b3f8.js b/assets/start.html-9ba0c0da.js similarity index 99% rename from assets/start.html-a4c1b3f8.js rename to assets/start.html-9ba0c0da.js index 8db8a10..1b91478 100644 --- a/assets/start.html-a4c1b3f8.js +++ b/assets/start.html-9ba0c0da.js @@ -1,4 +1,4 @@ -import{_ as n,o as s,c as a,e}from"./app-72107ff1.js";const p={},t=e(`

    app 测试

    seldom 3.0 基于appium支持APP测试。

    appium 官方网站:https://appium.io/

    环境安装

    app 的自动化测试环境相比较 web 要复杂一些,请参考appium官方。

    1. 安装node

    https://nodejs.org/en/

    > node --version
    +import{_ as n,o as s,c as a,e}from"./app-9fb6f1b5.js";const p={},t=e(`

    app 测试

    seldom 3.0 基于appium支持APP测试。

    appium 官方网站:https://appium.io/

    环境安装

    app 的自动化测试环境相比较 web 要复杂一些,请参考appium官方。

    1. 安装node

    https://nodejs.org/en/

    > node --version
     v16.17.0
     
    1. 安装appium
    > npm i --location=global appium  # appium 2.x
     
    1. 启动appium
    > appium server --address '127.0.0.1' -p 4723
    diff --git a/assets/start.html-dc3af13e.js b/assets/start.html-dc3af13e.js
    deleted file mode 100644
    index 2caf54d..0000000
    --- a/assets/start.html-dc3af13e.js
    +++ /dev/null
    @@ -1,75 +0,0 @@
    -import{_ as e,r as p,o,c as l,a as n,b as s,d as i,e as a}from"./app-72107ff1.js";const c={},u=a('

    开始使用

    前言

    seldom 非常适合个人接口自动化项目,它有以下优势。

    • 可以写更少的代码
    • 提供详细的运行日志
    • 提供专门为接口设计的断言
    • 强大的数据驱动
    • 自动生成HTML/XML测试报告
    • 支持生成随机数据
    • 支持har/swagger文件转case
    • 支持数据库操作

    这些是seldom支持的功能,我们只需要集成HTTP接口库,并提供强大的断言即可。seldom 2.0 加入了HTTP接口自动化测试支持。

    ',5),r={href:"https://docs.python-requests.org/en/master/",target:"_blank",rel:"noopener noreferrer"},d=a(`
    seldomrequests
    self.get()requests.get()
    self.post()requests.post()
    self.put()requests.put()
    self.delete()requests.delete()
    self.patch()requests.patch()
    self.session()requests.session()

    Seldom VS Request+unittest

    • unittest + requests 接口自动化示例:
    import unittest
    -import requests
    -
    -
    -class TestAPI(unittest.TestCase):
    -
    -    def test_get_method(self):
    -        payload = {'key1': 'value1', 'key2': 'value2'}
    -        r = requests.get("http://httpbin.org/get", params=payload)
    -        self.assertEqual(r.status_code, 200)
    -
    -
    -if __name__ == '__main__':
    -    unittest.main()
    -
    • seldom 接口自动化测试示例:
    # test_req.py
    -import seldom
    -
    -
    -class TestAPI(seldom.TestCase):
    -
    -    def test_get_method(self):
    -        payload = {'key1': 'value1', 'key2': 'value2'}
    -        self.get("http://httpbin.org/get", params=payload)
    -        self.assertStatusCode(200)
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(debug=True)
    -

    主要简化点在,接口的返回数据的处理。当然,seldom真正的优势在断言、日志和报告。

    运行测试

    打开debug模式seldom.run(debug=True) 运行上面的用例。

    
    -> python test_req.py
    -
    -              __    __
    -   ________  / /___/ /___  ____ ____
    -  / ___/ _ \\/ / __  / __ \\/ __ \` ___/
    - (__  )  __/ / /_/ / /_/ / / / / / /
    -/____/\\___/_/\\__,_/\\____/_/ /_/ /_/  v3.x.x
    ------------------------------------------
    -                             @itest.info
    -
    -test_get_method (test_req.TestAPI) ... 2023-02-14 23:37:07 request.py | INFO |
    --------------- Request -----------------[🚀]
    -2023-02-14 23:37:07 request.py | INFO | [method]: GET      [url]: http://httpbin.org/get
    -2023-02-14 23:37:07 request.py | DEBUG | [params]:
    - {
    -  "key1": "value1",
    -  "key2": "value2"
    -}
    -2023-02-14 23:37:08 request.py | INFO | -------------- Response ----------------[🛬️]
    -2023-02-14 23:37:08 request.py | INFO | successful with status 200
    -2023-02-14 23:37:08 request.py | DEBUG | [type]: json      [time]: 0.785683
    -2023-02-14 23:37:08 request.py | DEBUG | [response]:
    - {
    -  "args": {
    -    "key1": "value1",
    -    "key2": "value2"
    -  },
    -  "headers": {
    -    "Accept": "*/*",
    -    "Accept-Encoding": "gzip, deflate",
    -    "Host": "httpbin.org",
    -    "User-Agent": "python-requests/2.28.1",
    -    "X-Amzn-Trace-Id": "Root=1-63ebaaa4-325e25be64b104e770c25f8f"
    -  },
    -  "origin": "173.248.248.88",
    -  "url": "http://httpbin.org/get?key1=value1&key2=value2"
    -}
    -2023-02-14 23:37:08 case.py | INFO | 👀 assertStatusCode -> 200.
    -ok
    -
    -----------------------------------------------------------------------
    -Ran 1 test in 0.795s
    -
    -OK
    -2023-02-14 23:37:08 runner.py | SUCCESS | A run the test in debug mode without generating HTML report!
    -

    通过日志/报告都可以看到详细的HTTP接口调用信息。

    `,11);function k(v,m){const t=p("ExternalLinkIcon");return o(),l("div",null,[u,n("p",null,[s("Seldom 完全兼容 "),n("a",r,[s("Requests"),i(t)]),s(" API 如下:")]),d])}const _=e(c,[["render",k],["__file","start.html.vue"]]);export{_ as default}; diff --git a/assets/test_library.html-70c22988.js b/assets/test_library.html-70c22988.js deleted file mode 100644 index e3a13d5..0000000 --- a/assets/test_library.html-70c22988.js +++ /dev/null @@ -1,135 +0,0 @@ -import{_ as n,o as s,c as a,e as t}from"./app-72107ff1.js";const p={},e=t(`

    支持更多测试库

    seldom 集成了seleniumappiumrequests,他们都是非常优秀且成熟的库,这并不是说,你不能在seldom使用其他的测试库。

    seldom 作为一个测试框架,理论上可以与任何测试库一起使用。seldom提供的基础能力(数据驱动、随机数、测试报告、缓存...等)同样可以提升这些测试库的使用效率。

    使用playwright

    playwright就微软推出的优秀的 web UI 自动化测试库。

    官方地址: https://playwright.dev/

    • pip安装Playwright
    > pip install playwright
    -
    • playwright 安装浏览器以及驱动
    > playwright install
    -
    • 使用例子
    import seldom
    -from playwright.sync_api import sync_playwright
    -from playwright.sync_api import expect
    -
    -
    -class Playwright(seldom.TestCase):
    -
    -    def start(self):
    -        self.p = sync_playwright().start()
    -        self.chromium = self.p.chromium.launch()
    -        self.page = self.chromium.new_page()
    -
    -    def end(self):
    -        self.chromium.close()
    -        self.p.stop()
    -
    -    def test_playwright_start(self):
    -        """
    -        test playwright index page
    -        """
    -        self.page.goto("http://playwright.dev")
    -        expect(self.page).to_have_title("Fast and reliable end-to-end testing for modern web apps | Playwright")
    -
    -        get_started = self.page.locator('text=Get Started')
    -        expect(get_started).to_have_attribute('href', '/docs/intro')
    -        get_started.click()
    -
    -        expect(self.page).to_have_url('http://playwright.dev/docs/intro')
    -        # 截图
    -        screenshot_bytes = self.page.screenshot()
    -        self.screenshots(image=screenshot_bytes)
    -
    -    def test_playwright_todo(self):
    -        """
    -        test playwright todoMVC
    -        """
    -        self.page.goto("https://demo.playwright.dev/todomvc/#/")
    -        new_todo = self.page.locator(".new-todo")
    -        new_todo.fill("sleep")
    -        new_todo.press("Enter")
    -        new_todo.fill("code")
    -        new_todo.press("Enter")
    -        new_todo.fill("eat")
    -        new_todo.press("Enter")
    -        self.page.locator('li').filter(has_text='code').get_by_label('Toggle Todo').check()
    -        self.page.locator('li').filter(has_text='sleep').get_by_label('Toggle Todo').check()
    -        self.page.locator('li').filter(has_text='eat').get_by_label('Toggle Todo').check()
    -        # 截图
    -        screenshot_bytes = self.page.screenshot()
    -        self.screenshots(image=screenshot_bytes)
    -
    -
    -if __name__ == '__main__':
    -    seldom.main()
    -

    使用uiautomator2

    uiautomator2是openatx推出的优秀的Android自动化测试工具,Api简单,同样得到广泛应用。

    github地址: https://github.com/openatx/uiautomator2

    • pip安装
    pip install uiautomator2
    -
    • 使用例子
    import seldom
    -import uiautomator2 as u2
    -
    -
    -class MyAppTest(seldom.TestCase):
    -
    -    def start(self):
    -        # 链接设备
    -        self.d = u2.connect('192.168.31.234')
    -        # 启动App
    -        self.d.app_start("com.meizu.mzbbs")
    -
    -    def end(self):
    -        # 停止app
    -        self.d.app_stop("com.meizu.mzbbs")
    -
    -    def test_app(self, user):
    -        """ 使用 uiautomator2 """
    -        # 搜索
    -        self.d(resourceId="com.meizu.flyme.flymebbs:id/nw").click()
    -        # 输入关键字
    -        self.d(resourceId="com.meizu.flyme.flymebbs:id/nw").set_text("flyme")
    -        # 搜索按钮
    -        self.d(resourceId="com.meizu.flyme.flymebbs:id/o1").click()
    -        self.sleep(2)
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(debug=True)
    -

    使用pyAutoGUI

    pyAutoGUI专注于模拟鼠标和键盘操作,实现GUI的自动化。 适用于需要在多个操作系统(Windows、macOS、Linux)上模拟用户输入(如点击、拖动、输入文本等)的场景,如自动化测试、数据录入、游戏辅助等。

    官方地址: https://github.com/asweigart/pyautogui

    • pip安装pyAutoGUI
    > pip install pyautogui
    -
    • 使用例子
    import os
    -import pyautogui
    -import seldom
    -from seldom.testdata import get_int
    -
    -
    -class TestPyAutoGUINote(seldom.TestCase):
    -
    -    def start(self):
    -        # 打开记事本(这里使用运行命令来打开,确保路径正确)
    -        os.system('notepad.exe')
    -        self.sleep()
    -
    -    def end(self):
    -        # 模拟按下 Alt+F4 关闭记事本
    -        pyautogui.hotkey('alt', 'f4')
    -
    -    def test_write_and_save(self):
    -        """
    -        打开一个新的标签页写入内容并保存
    -        """
    -        # 模拟按下 Ctrl+t 创建一个新的标签页
    -        pyautogui.hotkey('ctrl', 't')
    -
    -        self.sleep()
    -        pyautogui.press('shift')  # 切换英文输入法
    -
    -        # 写入字符串到记事本
    -        pyautogui.write('Hello, this is a test string written by pyautogui.', interval=0.1)  # interval 参数设置每个字符之间的延迟时间
    -
    -        # 模拟按下 Ctrl+S 保存文件
    -        pyautogui.hotkey('ctrl', 's')
    -        self.sleep()
    -
    -        # 切换英文输入法
    -        pyautogui.press('shift')
    -        self.sleep()
    -
    -        # 输入文件名 + 回车确定
    -        pyautogui.write(f'test_file{get_int()}.txt')
    -        self.sleep()
    -        pyautogui.press('enter')
    -        self.sleep()
    -
    -
    -if __name__ == '__main__':
    -    seldom.main()
    -
    `,26),o=[e];function c(i,l){return s(),a("div",null,o)}const d=n(p,[["render",c],["__file","test_library.html.vue"]]);export{d as default}; diff --git a/assets/test_library.html-2407429c.js b/assets/test_library.html-dab90a67.js similarity index 99% rename from assets/test_library.html-2407429c.js rename to assets/test_library.html-dab90a67.js index 57fa275..ee3031c 100644 --- a/assets/test_library.html-2407429c.js +++ b/assets/test_library.html-dab90a67.js @@ -1,4 +1,4 @@ -import{_ as n,o as s,c as a,e as t}from"./app-a06a2d51.js";const p={},e=t(`

    支持更多测试库

    seldom 集成了seleniumappiumrequests,他们都是非常优秀且成熟的库,这并不是说,你不能在seldom使用其他的测试库。

    seldom 作为一个测试框架,理论上可以与任何测试库一起使用。seldom提供的基础能力(数据驱动、随机数、测试报告、缓存...等)同样可以提升这些测试库的使用效率。

    使用playwright

    playwright就微软推出的优秀的 web UI 自动化测试库。

    官方地址: https://playwright.dev/

    • pip安装Playwright
    > pip install playwright
    +import{_ as n,o as s,c as a,e as t}from"./app-9fb6f1b5.js";const p={},e=t(`

    支持更多测试库

    seldom 集成了seleniumappiumrequests,他们都是非常优秀且成熟的库,这并不是说,你不能在seldom使用其他的测试库。

    seldom 作为一个测试框架,理论上可以与任何测试库一起使用。seldom提供的基础能力(数据驱动、随机数、测试报告、缓存...等)同样可以提升这些测试库的使用效率。

    使用playwright

    playwright就微软推出的优秀的 web UI 自动化测试库。

    官方地址: https://playwright.dev/

    • pip安装Playwright
    > pip install playwright
     
    • playwright 安装浏览器以及驱动
    > playwright install
     
    • 使用例子
    import seldom
     from playwright.sync_api import sync_playwright
    diff --git a/assets/webscocket.html-8dbaee79.js b/assets/webscocket.html-8dbaee79.js
    deleted file mode 100644
    index 39c7ebc..0000000
    --- a/assets/webscocket.html-8dbaee79.js
    +++ /dev/null
    @@ -1,80 +0,0 @@
    -import{_ as n,o as s,c as a,e}from"./app-72107ff1.js";const t={},p=e(`

    WebSocket

    seldom > 3.6.0 支持该功能

    有些时间我们需要通过WebSocket实现长连接,很高兴的告诉告诉你seldom支持WebSocket测试了。

    WebSocket 生命周期

    WebSocket 生命周期中包含几个关键的事件,这些事件允许开发人员在连接的不同阶段执行代码。以下是WebSocket API中定义的主要事件:

    • open: 当WebSocket连接成功建立时触发。这个事件表明客户端与服务器之间的连接已经打开,可以开始数据传输。

    • message: 当客户端接收到服务器发送的消息时触发。这个事件用于处理从服务器接收到的所有消息。

    • error: 当发生错误,导致WebSocket连接关闭之前或连接无法成功建立时触发。这个事件可以用来处理和响应WebSocket过程中出现的任何异常或错误情况。

    • close: 当连接被关闭时触发,无论是客户端还是服务器端主动关闭连接,或是因为某种原因连接被迫关闭。这个事件表明WebSocket连接已经彻底关闭,可以进行清理和后续处理。

    seldom测试WebSocket

    在seldom中测试WebSocket非常简单。

    • 首先,需要一个WebSocket服务。

    通过aiohttp实现websocket_server.py

    # websocket_server.py
    -from aiohttp import web
    -import aiohttp
    -
    -
    -async def websocket_handler(request):
    -    ws = web.WebSocketResponse()
    -    await ws.prepare(request)
    -
    -    async for msg in ws:
    -        if msg.type == aiohttp.WSMsgType.TEXT:
    -            print("message", msg.data)
    -            if msg.data == 'close':
    -                await ws.close()
    -            else:
    -                await ws.send_str(f"Message text was: {msg.data}")
    -        elif msg.type == aiohttp.WSMsgType.ERROR:
    -            print('ws connection closed with exception %s' %
    -                  ws.exception())
    -
    -    print('websocket connection closed')
    -
    -    return ws
    -
    -
    -app = web.Application()
    -app.router.add_get('/ws', websocket_handler)
    -
    -web.run_app(app, port=8765)
    -
    • 然后,通过seldom编写WebSocket测试用例。
    import seldom
    -from seldom.logging import log
    -from seldom.websocket_client import WebSocketClient
    -
    -
    -class WebSocketTest(seldom.TestCase):
    -
    -    def start(self):
    -        # 创建WebSocket客户端线程
    -        self.client = WebSocketClient("ws://0.0.0.0:8765/ws")
    -        self.client.start()
    -        # 等待客户端连接建立
    -        self.sleep(1)  # 这里假设服务器可以在1秒内响应连接
    -
    -    def tearDown(self):
    -        # 发送关闭消息
    -        self.client.send_message("close")
    -        # 停止WebSocket客户端线程
    -        self.client.stop()
    -        self.client.join()
    -
    -    def test_send_and_receive_message(self):
    -        # 发送消息
    -        self.client.send_message("Hello, WebSocket!")
    -        self.client.join(1)  # 等待接收消息
    -        self.client.send_message("How are you?")
    -        self.client.join(1)  # 等待接收消息
    -        # 验证是否收到消息
    -        log.info(self.client.received_messages)
    -        self.assertEqual(len(self.client.received_messages), 2)
    -        self.assertIn("Hello, WebSocket!", self.client.received_messages[0])
    -        self.assertIn("How are you?", self.client.received_messages[1])
    -
    -
    -if __name__ == '__main__':
    -    seldom.main(debug=True)
    -
    • 运行日志
    > python test_websocket.py
    -
    -test_send_and_receive_message (test_websocket.WebSocketTest.test_send_and_receive_message) ... 
    -2024-04-05 23:36:33 | INFO     | case.py | 💤️ sleep: 1s.
    -2024-04-05 23:36:33 | INFO     | websocket_client.py | WebSocket connection opened.
    -2024-04-05 23:36:36 | INFO     | test_websocket.py | ['Message text was: Hello, WebSocket!', 'Message text was: How are you?']
    -ok
    -
    -----------------------------------------------------------------------
    -Ran 1 test in 3.006s
    -
    -OK
    -2024-04-05 23:36:36 | SUCCESS  | runner.py | A run the test in debug mode without generating HTML report!
    -
    -
    `,15),o=[p];function c(i,l){return s(),a("div",null,o)}const d=n(t,[["render",c],["__file","webscocket.html.vue"]]);export{d as default}; diff --git a/assets/webscocket.html-036a20ea.js b/assets/webscocket.html-e39e5dd8.js similarity index 99% rename from assets/webscocket.html-036a20ea.js rename to assets/webscocket.html-e39e5dd8.js index a4bdf2d..8a6bceb 100644 --- a/assets/webscocket.html-036a20ea.js +++ b/assets/webscocket.html-e39e5dd8.js @@ -1,4 +1,4 @@ -import{_ as n,o as s,c as a,e}from"./app-a06a2d51.js";const t={},p=e(`

    WebSocket

    seldom > 3.6.0 支持该功能

    有些时间我们需要通过WebSocket实现长连接,很高兴的告诉告诉你seldom支持WebSocket测试了。

    WebSocket 生命周期

    WebSocket 生命周期中包含几个关键的事件,这些事件允许开发人员在连接的不同阶段执行代码。以下是WebSocket API中定义的主要事件:

    • open: 当WebSocket连接成功建立时触发。这个事件表明客户端与服务器之间的连接已经打开,可以开始数据传输。

    • message: 当客户端接收到服务器发送的消息时触发。这个事件用于处理从服务器接收到的所有消息。

    • error: 当发生错误,导致WebSocket连接关闭之前或连接无法成功建立时触发。这个事件可以用来处理和响应WebSocket过程中出现的任何异常或错误情况。

    • close: 当连接被关闭时触发,无论是客户端还是服务器端主动关闭连接,或是因为某种原因连接被迫关闭。这个事件表明WebSocket连接已经彻底关闭,可以进行清理和后续处理。

    seldom测试WebSocket

    在seldom中测试WebSocket非常简单。

    • 首先,需要一个WebSocket服务。

    通过aiohttp实现websocket_server.py

    # websocket_server.py
    +import{_ as n,o as s,c as a,e}from"./app-9fb6f1b5.js";const t={},p=e(`

    WebSocket

    seldom > 3.6.0 支持该功能

    有些时间我们需要通过WebSocket实现长连接,很高兴的告诉告诉你seldom支持WebSocket测试了。

    WebSocket 生命周期

    WebSocket 生命周期中包含几个关键的事件,这些事件允许开发人员在连接的不同阶段执行代码。以下是WebSocket API中定义的主要事件:

    • open: 当WebSocket连接成功建立时触发。这个事件表明客户端与服务器之间的连接已经打开,可以开始数据传输。

    • message: 当客户端接收到服务器发送的消息时触发。这个事件用于处理从服务器接收到的所有消息。

    • error: 当发生错误,导致WebSocket连接关闭之前或连接无法成功建立时触发。这个事件可以用来处理和响应WebSocket过程中出现的任何异常或错误情况。

    • close: 当连接被关闭时触发,无论是客户端还是服务器端主动关闭连接,或是因为某种原因连接被迫关闭。这个事件表明WebSocket连接已经彻底关闭,可以进行清理和后续处理。

    seldom测试WebSocket

    在seldom中测试WebSocket非常简单。

    • 首先,需要一个WebSocket服务。

    通过aiohttp实现websocket_server.py

    # websocket_server.py
     from aiohttp import web
     import aiohttp
     
    diff --git a/develop.html b/develop.html
    index c7db1e7..fbc6290 100644
    --- a/develop.html
    +++ b/develop.html
    @@ -24,7 +24,7 @@
         
         seldom文档
         
    -    
    +    
       
       
         
    -    
    +    
       
     
    diff --git a/getting-started/advanced.html b/getting-started/advanced.html
    index 897aa6f..1297bf2 100644
    --- a/getting-started/advanced.html
    +++ b/getting-started/advanced.html
    @@ -24,10 +24,10 @@
         
         高级用法 | seldom文档
         
    -    
    +    
       
       
    -    

    高级用法

    fixture

    有时自动化测试用例的运行需要一些前置&后置步骤,seldom提供了相应的方法。

    seldom重写了unittest的fixture,所以,请使用seldom的fixture,对应表格。

    unittestseldom说明
    setUpClass(cls)start_class(cls)测试类开始执行。
    tearDownClass(cls)end_class(cls)测试类结束执行。
    setUp(self)start(self)测试方法(用例)开始执行。
    tearDown(self)end(self)测试方法(用例)结束执行。

    示例

    针对每条测试类/测试用例的fixture使用示例。

    # test_fixture.py
    +    

    高级用法

    fixture

    有时自动化测试用例的运行需要一些前置&后置步骤,seldom提供了相应的方法。

    seldom重写了unittest的fixture,所以,请使用seldom的fixture,对应表格。

    unittestseldom说明
    setUpClass(cls)start_class(cls)测试类开始执行。
    tearDownClass(cls)end_class(cls)测试类结束执行。
    setUp(self)start(self)测试方法(用例)开始执行。
    tearDown(self)end(self)测试方法(用例)结束执行。
    -start_run()confrun.py文件配置,整个用例开始前运行。
    -end_run()confrun.py文件配置,整个用例结束后运行。

    示例1

    针对每条测试类/测试用例的fixture使用示例。

    # test_fixture.py
     import seldom
     
     
    @@ -67,7 +67,51 @@
     ok
     测试类结束执行
     ...
    -

    跳过测试

    seldom 提供了跳过用例的装饰用于跳过暂时不执行的用例。

    装饰器

    • seldom.skip():无条件地跳过一个测试。
    • seldom.skip_if(): 如果条件为真,则跳过测试。
    • seldom.skip_unless(): 跳过一个测试,除非条件为真。
    • seldom.expected_failure(): 预期测试用例会失败。
    • self.skipTest(): 根据条件跳过测试。

    使用方法

    # test_skip.py
    +

    示例2

    有时候我们需要整个测试开始前结束后完成一些工作,可以通过下面的方式配置。

    • 目录结构
    mypro/
    +├── test_dir/
    +│   ├── __init__.py
    +│   ├── test_sample.py
    +├── confrun.py
    +└── run.py
    +
    • confrun.py 配置前后置动作
    from seldom.logging import log
    +from seldom.utils import cache
    +
    +
    +def start_run():
    +    """
    +    Test the hook function before running
    +    """
    +    log.info("start_run")
    +    cache.set({"token": "token123"})
    +
    +
    +def end_run():
    +    """
    +    Test the hook function after running
    +    """
    +    log.info("end_run")
    +    cache.clear("token")
    +

    示例中用于添加和清除 cache, 根据实际需求你可以加上任何动作。

    • run.py 执行用例
    import seldom
    +
    +if __name__ == '__main__':
    +    seldom.main(path="./test_dir")
    +
    • 运行结果
    > python run.py
    +...
    +
    +2024-12-06 17:55:04 | INFO     | confrun.py | MainThread | start_run   # confrun.py 所有用例前的动作
    +2024-12-06 17:55:04 | INFO     | cache.py | MainThread | 💾 Set cache data: token = token123
    +
    +2024-12-06 17:55:04 | INFO     | runner.py | MainThread | TestLoader: ./test_dir
    +XTestRunner Running tests...
    +----------------------------------------------------------------------
    +2024-12-06 17:55:04 | INFO     | cache.py | MainThread | 💾 Get cache data: token = token123
    +Generating HTML reports...
    +.12024-12-06 17:55:04 | SUCCESS  | runner.py | MainThread | generated html file: file:///D:\github\seldomQA\seldom\reports\2024_12_06_17_55_03_result.html
    +2024-12-06 17:55:04 | SUCCESS  | runner.py | MainThread | generated log file: file:///D:\github\seldomQA\seldom\reports\seldom_log.log
    +
    +2024-12-06 17:55:04 | INFO     | confrun.py | MainThread | end_run  # confrun.py 所有用例后的动作
    +2024-12-06 17:55:04 | INFO     | cache.py | MainThread | 💾 Clear cache data: token
    +

    跳过测试

    seldom 提供了跳过用例的装饰用于跳过暂时不执行的用例。

    装饰器

    • seldom.skip():无条件地跳过一个测试。
    • seldom.skip_if(): 如果条件为真,则跳过测试。
    • seldom.skip_unless(): 跳过一个测试,除非条件为真。
    • seldom.expected_failure(): 预期测试用例会失败。
    • self.skipTest(): 根据条件跳过测试。

    使用方法

    # test_skip.py
     import seldom
     
     
    @@ -489,6 +533,6 @@
         dc.clear("add")
         seldom.main(debug=True)
     
    - + diff --git a/getting-started/create_project.html b/getting-started/create_project.html index faaf528..699d96a 100644 --- a/getting-started/create_project.html +++ b/getting-started/create_project.html @@ -24,7 +24,7 @@ 创建项目 | seldom文档 - +

    创建项目

    seldom已经安装完成,那么现在已经迫不及待的想体验seldom的使用。

    自动生成项目

    seldom 通过seldom命令提供了脚手架,可以快速的帮我们创建自动化测试项目。

    1. 查看帮助:
    > seldom --help
    @@ -90,6 +90,6 @@
     if __name__ == '__main__':
         seldom.main()
     

    根据自己的需求编写Web UIApp UIHTTP接口自动化测试。

    - + diff --git a/getting-started/data_driver.html b/getting-started/data_driver.html index abbef99..042ccb7 100644 --- a/getting-started/data_driver.html +++ b/getting-started/data_driver.html @@ -24,7 +24,7 @@ 数据驱动 | seldom文档 - +

    数据驱动

    数据驱动是测试框架非常重要的功能之一,它可以有效的节约大量重复的测试代码。seldom针对该功能做强大的支持。

    @class_data() 方法

    class_data() 装饰测试类,测试类下面的任何方法可以共用 class_data() 中定义的变量。

    • 用法一
    import seldom
    @@ -377,6 +377,6 @@
     if __name__ == '__main__':
         seldom.main()
     

    更多的用法请查看 ddt 文档:https://ddt.readthedocs.io/en/latest/example.html

    - + diff --git a/getting-started/dependent_func.html b/getting-started/dependent_func.html index e2be60c..17960cb 100644 --- a/getting-started/dependent_func.html +++ b/getting-started/dependent_func.html @@ -24,7 +24,7 @@ 方法的依赖 | seldom文档 - +

    方法的依赖

    在 seldom 3.4.0 版本实现了该功能。

    在复杂的测试场景中,常常会存在用例依赖,以一个接口自动化平台为例,依赖关系:

    创建用例 --> 创建模块 --> 创建项目 --> 登录

    用例依赖的问题

    • 用例的依赖对于的执行顺序有严格的要求,比如让被依赖的方法先执行。
    • 一旦使用用例依赖,依赖的用例就无法单独执行了,按照用例的设计原则,每条用例都应该独立执行。

    正确的做法

    我们应该将依赖的操作封装成方法调用。如果能通过装饰器实现调用,那就很有趣了。

    aomakeropen in new window 提供了这种装饰器的实现,seldom 进行了复刻,只是的定位和用法用有所不同。

    类内部方法调用

    我们可以在测试类下面,创建普通的方法。然后通过@dependent_func()装饰器调用他。

    import seldom
    @@ -220,6 +220,6 @@
         seldom.main(debug=True)
         cache.clear()
     

    说明

    1. @data() 装饰器必须写在 @dependent_func() 的上面。
    2. 运行两条用例,user_login() 被执行过一次后,第二次则不需要重复执行,直接返回结果。
    - + diff --git a/getting-started/installation.html b/getting-started/installation.html index 24557ac..a947484 100644 --- a/getting-started/installation.html +++ b/getting-started/installation.html @@ -24,7 +24,7 @@ Installation | seldom文档 - +

    Installation

    seldom的安装非常简单。

    • 快速安装

    目前已经上传 pypi.org ,可以使用pip命令安装。

    > pip install seldom
    @@ -54,6 +54,6 @@
     Requires:  Appium-Python-Client, click, genson, jmespath, jsonschema, loguru, openpyxl, pymysql, python-dateutil, pyyaml, requests, websocket-client, XTestRunner
     Required-by:
     
    - + diff --git a/getting-started/quick_start.html b/getting-started/quick_start.html index f941437..d532f38 100644 --- a/getting-started/quick_start.html +++ b/getting-started/quick_start.html @@ -24,7 +24,7 @@ 快速开始 | seldom文档 - +

    快速开始

    基本规范

    seldom继承unittest单元测试框架,所以他的编写规范与unittestopen in new window基本保持一致。

    # test_sample.py
    @@ -40,7 +40,7 @@
     
     if __name__ == '__main__':
         seldom.main()
    -

    基本规范:

    1. 创建测试类YouTest并继承seldom.TestCase类。
    2. 创建测试方法test_case, 必须以test开头。
    3. seldom.mian()是框架运行的入口方法,接下来详细介绍。

    main() 方法

    main()方法是seldom运行测试的入口, 它提供了一些最基本也是最重要的配置。

    import seldom
    +

    基本规范:

    1. 创建测试类YouTest并继承seldom.TestCase类。
    2. 创建测试方法test_case, 必须以test开头。
    3. seldom.main()是框架运行的入口方法,接下来详细介绍。

    main() 方法

    main()方法是seldom运行测试的入口, 它提供了一些最基本也是最重要的配置。

    import seldom
     
     # ...
     
    @@ -211,7 +211,7 @@
         """Use case exe failed to stop, only support debug=True"""
         return False
     
    -

    以上配置根据需求自动化项目类型配置,相互可能冲突的钩子函数:

    • Web UI测试: browser()
    • http 接口测试: base_url()
    • app UI测试: app_info(), app_server()

    参数表格:

    seldom.main() (参数)confrun.py (函数)类型说明
    pathN/A通用指定测试目录或文件, 与case参数互斥。
    caseN/A通用指定测试用例, 与path参数互斥。
    browserbrowser()Web指定web测试运行的浏览器。
    base_urlbase_url()HTTP指定HTTP接口测试的基本URL。
    app_infoapp_info()Appapp 启动信息,参考appium desired_capabilities配置, app测试。
    app_serverapp_server()Appappium server 启动地址(默认 http://127.0.0.1:4723), app测试。
    reportreport()通用自定义测试报告的名称,例如result.html/result.xml
    titletitle()通用指定HTML报告标题。
    testertester()通用指定HTML报告测试人员。
    descriptiondescription()通用指定HTML报告描述。
    languagelanguage()通用设置HTML报告中英文,默认en, 中文zh-CN
    debugdebug()通用debug模式,设置为True不生成测试HTML测试,默认为False
    rerunrerun()通用设置失败重新运行次数。
    timeouttimeout()通用设置自动化全局超时时间,默认10秒。作用于元素定位、断言等。
    whitelistwhitelist()通用用例标签(label)设置白名单。
    blacklistblacklist()通用用例标签(label)设置黑名单。
    openN/A通用是否使用浏览器自动打开测试报告,默认True
    N/Amock_url()HTTP定义mock URL 映射。

    运行测试

    seldom 的运行有三种方式:

    • main() 方法:在.py 文件中使用seldom.main() 方法。
    • seldom 命令:通过sedom 命令指定要运行的目录&文件&类&方法。
    • pycharm右键执行:这种方式无法读取到配置,有严重缺陷。

    强烈建议使用前两种!!

    1. main()方法运行测试

    • 目录结构
    mypro/
    +

    以上配置根据需求自动化项目类型配置,相互可能冲突的钩子函数:

    • Web UI测试: browser()
    • http 接口测试: base_url()
    • app UI测试: app_info(), app_server()

    参数表格:

    seldom.main() (参数)confrun.py (函数)类型说明
    path-通用指定测试目录或文件, 与case参数互斥。
    case-通用指定测试用例, 与path参数互斥。
    browserbrowser()Web指定web测试运行的浏览器。
    base_urlbase_url()HTTP指定HTTP接口测试的基本URL。
    -mock_url()HTTP配置HTTP接口 mock URL。
    -proxies()HTTP配置HTTP接口proxies代理。
    app_infoapp_info()Appapp 启动信息,参考appium desired_capabilities配置, app测试。
    app_serverapp_server()Appappium server 启动地址(默认 http://127.0.0.1:4723), app测试。
    reportreport()通用自定义测试报告的名称,例如result.html/result.xml
    titletitle()通用指定HTML报告标题。
    testertester()通用指定HTML报告测试人员。
    descriptiondescription()通用指定HTML报告描述。
    languagelanguage()通用设置HTML报告中英文,默认en, 中文zh-CN
    debugdebug()通用debug模式,设置为True不生成测试HTML测试,默认为False
    rerunrerun()通用设置失败重新运行次数。
    timeouttimeout()通用设置自动化全局超时时间,默认10秒。作用于元素定位、断言等。
    whitelistwhitelist()通用用例标签(label)设置白名单。
    blacklistblacklist()通用用例标签(label)设置黑名单。
    open-通用是否使用浏览器自动打开测试报告,默认True

    运行测试

    seldom 的运行有三种方式:

    • main() 方法:在.py 文件中使用seldom.main() 方法。
    • seldom 命令:通过sedom 命令指定要运行的目录&文件&类&方法。
    • pycharm右键执行:这种方式无法读取到配置,有严重缺陷。

    强烈建议使用前两种!!

    1. main()方法运行测试

    • 目录结构
    mypro/
     ├── test_dir/
     │   ├── __init__.py
     │   ├── test_sample.py
    @@ -385,6 +385,6 @@
         for p in paths:
             run_case(p)
     
    - + diff --git a/getting-started/seldom_cli.html b/getting-started/seldom_cli.html index 5a7b438..5b639e1 100644 --- a/getting-started/seldom_cli.html +++ b/getting-started/seldom_cli.html @@ -24,7 +24,7 @@ seldom CLI | seldom文档 - +

    seldom CLI

    seldom 2.10.7 对命令行工具做了增强,可以使用命令行的方式运行用例。

    seldom 帮助

    • seldom --help 查看帮助使用
    > seldom --help
    @@ -113,6 +113,6 @@
     
    • 说明:
      • -p/--path: 指定运行用例的根目录:test_dir
      • -j/--case-json: 运行收集用例文件: case.json
      • -r/--report: 运行收集用例生成报告: result.html

    清除所有缓存

    > seldom --clear-cache true
     
    • 说明:默认清空seldom所有缓存,即cache.clear()

    执行 API(excel文件)测试用例

    > seldom  --api-excel api_case.xlsx
     
    • 说明:简单的HTTP接口测试可以使用excel编写,seldom支持运行excel文件。excel的具体定义可以参考HTTP接口测试章节。
    - + diff --git a/index.html b/index.html index 9442427..c5e37c2 100644 --- a/index.html +++ b/index.html @@ -24,10 +24,10 @@ seldom文档 - +
    Seldom

    Seldom

    seldom 是基于unittest 的自动化测试框架。

    快速上手→ 项目简介

    web/app/api 测试

    seldom是一个全功能测试框架。

    快速

    提供脚手架快速创建自动化项目。

    报告

    集成XTestRunner测试报告,现代美观。

    断言

    提供丰富的断言,方便验证测试结果。

    数据驱动

    支持Excel/CSV/JSON/YAML数据文件。

    平台化

    seldom提供了平台化支持。

    - + diff --git a/introduce.html b/introduce.html index 8845ea3..8ff823e 100644 --- a/introduce.html +++ b/introduce.html @@ -24,10 +24,10 @@ 介绍 | seldom文档 - + -

    介绍

    新书推荐

    京东链接

    京东 购买链接open in new window

    天猫 购买链接open in new window

    当当 购买链接open in new window

    依托于 SeldomQA 相关项目的开发和维护,在 自动化测试框架设计定制化测试报告设计设计模式,以及测试平台开发 方面有着深厚技术积累和独特的设计理念。

    一本真正介绍 自动化测试框架设计 的书终于出版了,书中浅显易懂的介绍了 SeldomQA 相关项目中的诸多设计和封装技术。并且,介绍了一个开源自动化测试框架从设计到发布的整个流程。 如果你正在使用SeldomQA相关项目之余,想了解他们背后的设计,那么这本书非常值得购买。

    seldom框架

    特点

    seldom 是基于 unittest 的全功能自动化测试框架;针对自动化测试达到开箱即用。

    seldom特点

    • 支持测试类型(web/app/api)
    • 丰富的断言
    • 生成随机测试数据
    • 用例依赖
    • 用例分类标签
    • 支持发送(邮件、钉钉、飞书、企微)消息等
    • 日志打印
    • 缓存cache
    • 命令行工具
    • 强大的数据驱动(JSON/YAML/CSV/EXCEL)
    • HTML/XML报告
    • 失败重跑&截图
    • 数据库操作(MySQL/sqlite3/Mongodb)
    • 支持平台化

    设计理念

    简单一句话就是回到最初写代码的样子。

    自动化测试框架很多,只有在测试领域有一个比较奇怪的现象,如何用不写代码的方式解决自动化问题。为此,我们发明了用特定领域语言写用例,发明了用 excel 写用例,发明了用 YAML/JSON 写用例。这些方案看似简化了用例的编写,但是,会让解决复杂的问题变得更复杂。比如实现个分支判断/循环,传递参数,调用封装的步骤,编程语言中用 if/for 、变量、函数就实现了,但是用非编程语言的方式写用例处理起来就很麻烦。最终,并不能完全脱离编程,那么为什么不一开始就选择一个编程框架呢?

    然而,seldom的定位是尽量用简单的设计去解决复杂问题,例如 Flask、requests、yagmail...等,这些框架/库都有一个共同的特点,用简单的方式去解决复杂的问题,在编程语言这个层面,并不会给你太多限制,你可以完全使用它,也可以只用一部分,也可以平滑的实现它不支持的功能。

    seldom的目标以就让你用最少的代码编写自动化测试用例,当遇到seldom没有的功能,你可以方便的进行扩展。-- 这就是seldom的设计理念。

    发展历史

    2015年7月15号我在github上提交一个自动化项目,命名为:pyse, 即各取了pythonselenium前两个字符。项目非常简单核心就三个文件。

    • pyse.py:针对 selenium API做了简单封装。
    • HTMLTestRunner.py: 修改的HTMLTestRunner报告。
    • TestRunner.py: 一个简单的 unittest运行器。

    之后项目断断续续的在维护,直到2019年,也许是太闲了,加上对UI自动化有了更深入的理解,重新投入主要精力维护pyse项目。

    后来就需要将提交到pypi,这样更方便通过pip安装,发现 pyse 早已经被占用了,后来更名为seldom ,其实命名没有太多寓意,就是看他长得和selenium比较接近。

    2020年1月发布1.0版本,之所以发布1.0 是因为自认为框架的功能比较成熟了,并且花费时间补充了文档。大家都不重视文档,其实文档非常重要,也需要花大量的时间更新和维护。有时候你加个功能很简单,编写说明文档和使用示例就要花费等同的时间。

    1.0 版本之后,项目核心围绕着 selenium API的封装 和 unittest框架扩展(seldom基于unittest)等。

    2021年4月正式发布 2.0,集成requests, 正式支持http接口测试。起因是发现cypress支持http调用,哦,原来UI测试工具也可以去做接口,格局一下子打开了!如何在不影响现有selenium API的情况下集成requests是2.0考虑的重点。

    2022年1月seldom项目正式在公司内部推广使用,当时我们做了几版的接口测试平台,平台的开发维护成本比较高,对于复杂的场景用例,编写成本比框架还要复杂简单;功能也依赖于平台所提供的,相比较而言,框架却有最大的灵活性,可以很好的基于业务做各种设计和封装。

    因为在公司得到推广使用,seldom明显进入了更加快速的迭代开发阶段,并且稳定性、可用性灰得到了很大的提升。

    seldom 3.0 背景 seldom集成App测试是顺理成章的事情,早在几个月前我已经在公司项目中尝试 seldom + appium 进行App自动化测试。App自动化的维护成本确实比接口要高许多,这是由App本身的特点决定的,框架很难做到实质上的改变。

    2022年10月seldom 3.0 beta发布,之所以选择appium有几个原因:

    • appium 是由商业工具在维护,历史比较长,不会随意停止维护。
    • appium 应用更加广泛,使用得人更多,支持得平台多(android/ios/flutter)
    • appium 继承selenium,对于seldom来说对原有API改动最小。

    目前,seldom 3.0 正式版已经发布,欢迎使用。

    seldom vs pytest

    seldom 是建立在 unittest 的基础上的自动化测试框架。与 pytest进行对比,无疑相当于像拿一台电脑与一颗 intel CPU 进行比较,虽然 intel CPU 很强大,但我们无法直接拿一个CPU打游戏,对吧? pytest 就像一个 CPU ,虽然很强大,但无法直接拿来做自动化测试,比如配合各种测试库。而seldom不需要额外安装测试库,即可开始编写自动化测试用例。

    • seldom vs pytest 对比差异
    功能seldompytest
    web UI测试支持 ✅支持(需安装 selenium) ⚠️
    web UI断言支持(assertText、assertTitle、assertElement) ✅不支持 ❌
    playwright支持(需安装playwright) ⚠️支持(playwright提供playwright-pytest插件) ✅
    失败截图支持(自动实现) ✅支持(需要设置) ✅
    http接口测试支持 ✅支持(需安装 requests) ⚠️
    http接口断言支持(assertJSON、assertPath、assertSchema) ✅不支持 ❌
    app UI测试支持 ✅支持(需安装 appium) ⚠️
    Page Object模式支持(推荐poium) ✅支持(推荐poium) ✅
    脚手架支持(快速创建项目) ✅不支持 ❌
    生成随机测试数据支持testdata不支持 ❌
    发送消息支持(email、钉钉、飞书、微信)✅不支持 ❌
    log日志支持 ✅不支持 ❌
    数据库操作支持(sqlite3、MySQL、SQL Server) ✅不支持 ❌
    用例依赖支持@depend()@pytest.mark.dependency()支持 ✅
    失败重跑支持rerunpytest-rerunfailures 支持 ✅
    用例分类标签支持@label()@pytest.mark.xxx支持 ✅
    HTML测试报告支持 ✅pytest-html、allure ✅
    XML测试报告支持 ✅自带 --junit-xml
    数据驱动方法@data()@pytest.mark.parametrize()
    数据驱动文件@file_data()(JSON\YAML\CSV\Excel) ✅不支持 ❌
    钩子函数confrun.py用例运行钩子 ⚠️conftest.py 功能更强大 ✅
    命令行工具CLI支持seldom支持pytest
    并发执行不支持 ❌pytest-xdist、pytest-parallel ✅
    平台化支持(seldom-platform)✅不支持 ❌
    第三方插件seldom(unittest)的生态比较糟糕 ⚠️pytest有丰富插件生态 ✅

    说明

    • ✅ : 表示支持。

    • ⚠️: 支持,但支持的不好,或没有对方好。

    • ❌ : 不支持,表示框架没有该功能,第三方插件也没有。

    - +

    介绍

    新书推荐

    京东链接

    京东 购买链接open in new window

    天猫 购买链接open in new window

    当当 购买链接open in new window

    依托于 SeldomQA 相关项目的开发和维护,在 自动化测试框架设计定制化测试报告设计设计模式,以及测试平台开发 方面有着深厚技术积累和独特的设计理念。

    一本真正介绍 自动化测试框架设计 的书终于出版了,书中浅显易懂的介绍了 SeldomQA 相关项目中的诸多设计和封装技术。并且,介绍了一个开源自动化测试框架从设计到发布的整个流程。 如果你正在使用SeldomQA相关项目之余,想了解他们背后的设计,那么这本书非常值得购买。

    seldom框架

    特点

    seldom 是基于 unittest 的全功能自动化测试框架;针对自动化测试达到开箱即用。

    seldom特点

    • 支持测试类型(web/app/api)
    • 丰富的断言
    • 生成随机测试数据
    • 用例依赖
    • 用例分类标签
    • 支持发送(邮件、钉钉、飞书、企微)消息等
    • 日志打印
    • 缓存cache
    • 命令行工具
    • 强大的数据驱动(JSON/YAML/CSV/EXCEL)
    • HTML/XML报告
    • 失败重跑&截图
    • 数据库操作(MySQL/sqlite3/Mongodb)
    • 支持平台化

    设计理念

    简单一句话就是回到最初写代码的样子。

    自动化测试框架很多,只有在测试领域有一个比较奇怪的现象,如何用不写代码的方式解决自动化问题。为此,我们发明了用特定领域语言写用例,发明了用 excel 写用例,发明了用 YAML/JSON 写用例。这些方案看似简化了用例的编写,但是,会让解决复杂的问题变得更复杂。比如实现个分支判断/循环,传递参数,调用封装的步骤,编程语言中用 if/for 、变量、函数就实现了,但是用非编程语言的方式写用例处理起来就很麻烦。最终,并不能完全脱离编程,那么为什么不一开始就选择一个编程框架呢?

    然而,seldom的定位是尽量用简单的设计去解决复杂问题,例如 Flask、requests、yagmail...等,这些框架/库都有一个共同的特点,用简单的方式去解决复杂的问题,在编程语言这个层面,并不会给你太多限制,你可以完全使用它,也可以只用一部分,也可以平滑的实现它不支持的功能。

    seldom的目标以就让你用最少的代码编写自动化测试用例,当遇到seldom没有的功能,你可以方便的进行扩展。-- 这就是seldom的设计理念。

    发展历史

    2015年7月15号我在github上提交一个自动化项目,命名为:pyse, 即各取了pythonselenium前两个字符。项目非常简单核心就三个文件。

    • pyse.py:针对 selenium API做了简单封装。
    • HTMLTestRunner.py: 修改的HTMLTestRunner报告。
    • TestRunner.py: 一个简单的 unittest运行器。

    之后项目断断续续的在维护,直到2019年,也许是太闲了,加上对UI自动化有了更深入的理解,重新投入主要精力维护pyse项目。

    后来就需要将提交到pypi,这样更方便通过pip安装,发现 pyse 早已经被占用了,后来更名为seldom ,其实命名没有太多寓意,就是看他长得和selenium比较接近。

    2020年1月发布1.0版本,之所以发布1.0 是因为自认为框架的功能比较成熟了,并且花费时间补充了文档。大家都不重视文档,其实文档非常重要,也需要花大量的时间更新和维护。有时候你加个功能很简单,编写说明文档和使用示例就要花费等同的时间。

    1.0 版本之后,项目核心围绕着 selenium API的封装 和 unittest框架扩展(seldom基于unittest)等。

    2021年4月正式发布 2.0,集成requests, 正式支持http接口测试。起因是发现cypress支持http调用,哦,原来UI测试工具也可以去做接口,格局一下子打开了!如何在不影响现有selenium API的情况下集成requests是2.0考虑的重点。

    2022年1月seldom项目正式在公司内部推广使用,当时我们做了几版的接口测试平台,平台的开发维护成本比较高,对于复杂的场景用例,编写成本比框架还要复杂简单;功能也依赖于平台所提供的,相比较而言,框架却有最大的灵活性,可以很好的基于业务做各种设计和封装。

    因为在公司得到推广使用,seldom明显进入了更加快速的迭代开发阶段,并且稳定性、可用性灰得到了很大的提升。

    seldom 3.0 背景 seldom集成App测试是顺理成章的事情,早在几个月前我已经在公司项目中尝试 seldom + appium 进行App自动化测试。App自动化的维护成本确实比接口要高许多,这是由App本身的特点决定的,框架很难做到实质上的改变。

    2022年10月seldom 3.0 beta发布,之所以选择appium有几个原因:

    • appium 是由商业工具在维护,历史比较长,不会随意停止维护。
    • appium 应用更加广泛,使用得人更多,支持得平台多(android/ios/flutter)
    • appium 继承selenium,对于seldom来说对原有API改动最小。

    目前,seldom 3.0 正式版已经发布,欢迎使用。

    seldom vs pytest

    seldom 是建立在 unittest 的基础上的自动化测试框架。与 pytest进行对比,无疑相当于像拿一台电脑与一颗 intel CPU 进行比较,虽然 intel CPU 很强大,但我们无法直接拿一个CPU打游戏,对吧? pytest 就像一个 CPU ,虽然很强大,但无法直接拿来做自动化测试,比如配合各种测试库。而seldom不需要额外安装测试库,即可开始编写自动化测试用例。

    • seldom vs pytest 对比差异
    功能seldompytest
    web UI测试支持 ✅支持(需安装 selenium) ⚠️
    web UI断言支持(assertText、assertTitle、assertElement) ✅不支持 ❌
    playwright支持(需安装playwright) ⚠️支持(playwright提供playwright-pytest插件) ✅
    失败截图支持(自动实现) ✅支持(需要设置) ✅
    http接口测试支持 ✅支持(需安装 requests) ⚠️
    http接口断言支持(assertJSON、assertPath、assertSchema) ✅不支持 ❌
    app UI测试支持 ✅支持(需安装 appium) ⚠️
    Page Object模式支持(推荐poium) ✅支持(推荐poium) ✅
    脚手架支持(快速创建项目) ✅不支持 ❌
    生成随机测试数据支持testdata不支持 ❌
    发送消息支持(email、钉钉、飞书、微信)✅不支持 ❌
    log日志支持 ✅不支持 ❌
    数据库操作支持(sqlite3、MySQL、SQL Server) ✅不支持 ❌
    用例依赖支持@depend()@pytest.mark.dependency()支持 ✅
    失败重跑支持rerunpytest-rerunfailures 支持 ✅
    用例分类标签支持@label()@pytest.mark.xxx支持 ✅
    HTML测试报告支持 ✅pytest-html、allure ✅
    XML测试报告支持 ✅自带 --junit-xml
    数据驱动方法@data()@pytest.mark.parametrize()
    数据驱动文件@file_data()(JSON\YAML\CSV\Excel) ✅不支持 ❌
    钩子函数confrun.py用例运行钩子 ⚠️conftest.py 功能更强大 ✅
    命令行工具CLI支持seldom支持pytest
    并发执行不支持 ❌pytest-xdist、pytest-parallel ✅
    平台化支持(seldom-platform)✅不支持 ❌
    第三方插件seldom(unittest)的生态比较糟糕 ⚠️pytest有丰富插件生态 ✅

    说明

    • ✅ : 表示支持。

    • ⚠️: 支持,但支持的不好,或没有对方好。

    • ❌ : 不支持,表示框架没有该功能,第三方插件也没有。

    + diff --git a/more-ability/db_operation.html b/more-ability/db_operation.html index ef41fed..a573947 100644 --- a/more-ability/db_operation.html +++ b/more-ability/db_operation.html @@ -24,7 +24,7 @@ 数据库操作 | seldom文档 - +

    数据库操作

    seldom 支持sqlite3、MySQL、SQL Server、MongoDB、PostgreSQL数据库操作。

    sqlite3MySQLSQL ServerPostgreSQL
    execute_sql()execute_sql()execute_sql()execute_sql()
    query_sql()query_sql()query_sql()query_sql()
    query_one()query_one()query_one()query_one()
    insert_get_last_id()insert_get_last_id()insert_get_last_id()insert_get_last_id()
    delete()delete()delete()delete()
    insert()insert()insert()insert()
    select()select()select()select()
    update()update()update()update()
    init_table()init_table()init_table()init_table()
    close()close()close()close()

    连接数据库

    连接sqlit3数据库

    from seldom.db_operation import SQLiteDB
    @@ -91,6 +91,6 @@
     print("table one data:", data)
     

    结果:

    table data: {'_id': 11, 'switch_notice': True, 'is_mock_open': False, 'strice': False, 'is_json5': False, 'name': '发布会签到系统'}
     
    - + diff --git a/more-ability/test_library.html b/more-ability/test_library.html index b484143..a4289ee 100644 --- a/more-ability/test_library.html +++ b/more-ability/test_library.html @@ -24,7 +24,7 @@ 支持更多测试库 | seldom文档 - +

    支持更多测试库

    seldom 集成了seleniumappiumrequests,他们都是非常优秀且成熟的库,这并不是说,你不能在seldom使用其他的测试库。

    seldom 作为一个测试框架,理论上可以与任何测试库一起使用。seldom提供的基础能力(数据驱动、随机数、测试报告、缓存...等)同样可以提升这些测试库的使用效率。

    使用playwright

    playwright就微软推出的优秀的 web UI 自动化测试库。

    官方地址: https://playwright.dev/

    • pip安装Playwright
    > pip install playwright
    @@ -162,6 +162,6 @@
     if __name__ == '__main__':
         seldom.main()
     
    - + diff --git a/platform/platform.html b/platform/platform.html index 864138b..fb27b46 100644 --- a/platform/platform.html +++ b/platform/platform.html @@ -24,10 +24,10 @@ 平台化支持 | seldom文档 - + -

    平台化支持

    为了更好的支持测试用例平台化,Seldom 提供了API用于获取用例列表,以及根据传入的用例信息运行测试用例。

    目录结构如下:

    mypro/
    +    

    平台化支持

    为了更好的支持测试用例平台化,Seldom 提供了API用于获取用例列表,以及根据传入的用例信息运行测试用例。

    seldom-platform: https://github.com/SeldomQA/seldom-platform

    目录结构如下:

    mypro/
     ├── test_dir/
     │   ├── module_api/
     │   │   ├── test_http_demo.py
    @@ -45,74 +45,80 @@
         case_info = main_extend.collect_cases(json=True)
         print(case_info)
     

    说明

    返回的用例信息列表:

    • collectCaseInfocollectCaseInfo设置为True 说明需要收集用例信息。
    • TestMainExtend(path="./test_dir/")TestMainExtend类是TestMain类的扩展,path设置收集用例的目录,不能为空。
    • collect_cases(json=False, level="data", warning=False):返回收集的用例信息。
      • json=False:默认为list格式;设置为True返回json格式。
      • level="data" :默认为data,数据驱动的每条数据被解析为一条用例。如果设置为 method 数据驱动的方法被解析为一条用例。
      • warning=False: 默认为False, 在收集用例的过程中,因为缺少依赖库,或导包错误会导致部分用例收集报错,是否要将这些错误保存下来。开启(True)后,默认保存在reports/collect_warning.log 文件中。
    [
    -    {
    -        "file": "module_api.test_http_demo",
    -        "class": {
    -            "name": "TestRequest",
    -            "doc": "\n    http api test demo\n    doc: https://requests.readthedocs.io/en/master/\n    "
    -        },
    -        "method": {
    -            "name": "test_get_method",
    -            "doc": "\n        test get request\n        "
    -        }
    +  {
    +    "file": "module_api.test_http_demo",
    +    "class": {
    +      "name": "TestRequest",
    +      "doc": "\n    http api test demo\n    doc: https://requests.readthedocs.io/en/master/\n    "
         },
    -    {
    -        "file": "module_api.test_http_demo",
    -        "class": {
    -            "name": "TestRequest",
    -            "doc": "\n    http api test demo\n    doc: https://requests.readthedocs.io/en/master/\n    "
    -        },
    -        "method": {
    -            "name": "test_post_method",
    -            "doc": "\n        test post request\n        "
    -        }
    +    "method": {
    +      "name": "test_get_method",
    +      "doc": "\n        test get request\n        ",
    +      "label": null
    +    }
    +  },
    +  {
    +    "file": "module_api.test_http_demo",
    +    "class": {
    +      "name": "TestRequest",
    +      "doc": "\n    http api test demo\n    doc: https://requests.readthedocs.io/en/master/\n    "
         },
    -    {
    -        "file": "module_web.test_ddt_demo",
    -        "class": {
    -            "name": "BaiduTest",
    -            "doc": "Baidu search test case"
    -        },
    -        "method": {
    -            "name": "test_baidu_0",
    -            "doc": "used parameterized test [with name=1, search_key='seldom']\n        :param name: case name\n        :param search_key: search keyword\n        "
    -        }
    +    "method": {
    +      "name": "test_post_method",
    +      "doc": "\n        test post request\n        ",
    +      "label": null
    +    }
    +  },
    +  {
    +    "file": "module_web.test_ddt_demo",
    +    "class": {
    +      "name": "BaiduTest",
    +      "doc": "Baidu search test case"
         },
    -    {
    -        "file": "module_web.test_ddt_demo",
    -        "class": {
    -            "name": "BaiduTest",
    -            "doc": "Baidu search test case"
    -        },
    -        "method": {
    -            "name": "test_baidu_1",
    -            "doc": "used parameterized test [with name=2, search_key='selenium']\n        :param name: case name\n        :param search_key: search keyword\n        "
    -        }
    +    "method": {
    +      "name": "test_baidu_0",
    +      "doc": "used parameterized test [with name=1, search_key='seldom']\n        :param name: case name\n        :param search_key: search keyword\n        ",
    +      "label": null
    +    }
    +  },
    +  {
    +    "file": "module_web.test_ddt_demo",
    +    "class": {
    +      "name": "BaiduTest",
    +      "doc": "Baidu search test case"
         },
    -    {
    -        "file": "module_web.test_ddt_demo",
    -        "class": {
    -            "name": "BaiduTest",
    -            "doc": "Baidu search test case"
    -        },
    -        "method": {
    -            "name": "test_baidu_2",
    -            "doc": "used parameterized test [with name=3, search_key='unittest']\n        :param name: case name\n        :param search_key: search keyword\n        "
    -        }
    +    "method": {
    +      "name": "test_baidu_1",
    +      "doc": "used parameterized test [with name=2, search_key='selenium']\n        :param name: case name\n        :param search_key: search keyword\n        ",
    +      "label": null
    +    }
    +  },
    +  {
    +    "file": "module_web.test_ddt_demo",
    +    "class": {
    +      "name": "BaiduTest",
    +      "doc": "Baidu search test case"
         },
    -    {
    -        "file": "module_web.test_first_demo",
    -        "class": {
    -            "name": "BaiduTest",
    -            "doc": "Baidu search test case"
    -        },
    -        "method": {
    -            "name": "test_case",
    -            "doc": "a simple test case "
    -        }
    +    "method": {
    +      "name": "test_baidu_2",
    +      "doc": "used parameterized test [with name=3, search_key='unittest']\n        :param name: case name\n        :param search_key: search keyword\n        ",
    +      "label": null
         }
    +  },
    +  {
    +    "file": "module_web.test_first_demo",
    +    "class": {
    +      "name": "BaiduTest",
    +      "doc": "Baidu search test case"
    +    },
    +    "method": {
    +      "name": "test_case",
    +      "doc": "a simple test case ",
    +      "label": null
    +    }
    +  }
     ]
    -

    数据结构说明:

    • file: 获取类的文件名,包含目录名。
    • class: 测试类的名字name 和 描述doc
    • method: 测试方法的名字name 和 描述doc

    执行用例信息

    当获取用例信息之后,可以进行自定义,例如 挑选出需要执行的用例,重新传给Seldom 执行。

    # run.py
    +

    数据结构说明:

    • file: 获取类的文件名,包含目录名。
    • class: 测试类的名字name 和 描述doc
    • method: 测试方法的名字name 和 描述doc, label

    注明:seldom==3.11.0 版本测试方法增加label字段。

    执行用例信息

    当获取用例信息之后,可以进行自定义,例如 挑选出需要执行的用例,重新传给Seldom 执行。

    # run.py
     from seldom import TestMainExtend
     
     if __name__ == '__main__':
    @@ -126,13 +132,60 @@
                 },
                 "method": {
                     "name": "test_case",
    -                "doc": "a simple test case "
    +                "doc": "a simple test case ",
    +                "label": ""
                 }
             }
         ]
         main_extend = TestMainExtend(path="./test_dir")
         main_extend.run_cases(cases)
    -

    说明:

    • cases 定义要执行的用例信息, doc 非必填字段。
    • TestMainExtend(path="./test_dir") : 其中path指定从哪个目录查找用例集合。
    • run_cases(cases): 运行用例。

    相关项目

    seldom-platform: https://github.com/SeldomQA/seldom-platform

    - +

    说明:

    • cases 定义要执行的用例信息, doc 非必填字段。
    • TestMainExtend(path="./test_dir") : 其中path指定从哪个目录查找用例集合。
    • run_cases(cases): 运行用例。

    接入平台比读

    如果你只是使用seldom框架编写用例,那么代码只要框架能运行即可,如果要接入seldom-platform平台,那么需要注意一下几点。

    🚧 测试每个子目录必须包含__init__.py文件。

    • 目录结构
    ├───reports
    +├───test_data
    +├───test_dir
    +│   ├───api_case
    +│   │   └───__init__.py
    +│   ├───app_case
    +│   │   └───__init__.py
    +│   ├───web_case
    +│   │   └───__init__.py
    +│   └───__init__.py
    +└───run.py
    +

    如果子目录不添加 init.py 文件会导致目录下面的用例无法解析。

    🚧 用例的前置动作

    在用 seldom框架写用例的时候需要执行一些前置/后置动作。

    import seldom
    +from seldom.utils import cache
    +
    +if __name__ == '__main__':
    +    # 前置动作
    +    cache.set({"token": "token123"})
    +    # 执行用例
    +    seldom.main("./test_dir")
    +    # 后置动作
    +    cache.clear("token")
    +

    但是,平台执行的时候,不会执行 前置/后置动作。 那么,为了使平台可以执行前置动作,需要使用confrun.py文件进行配置。

    • 目录结构
    ├───reports
    +├───test_data
    +├───test_dir
    +│   ├───...
    +├───confrun.py
    +└───run.py
    +
    • confrun.py配置
    """
    +seldom confrun.py  hooks function
    +"""
    +from seldom.utils import cache
    +
    +
    +def start_run():
    +    """前置动作"""
    +    cache.set({"token": "token123"})
    +
    +
    +def end_run():
    +    """后置动作"""
    +    cache.clear("token")
    +
    • run.py文件
    import seldom
    +
    +if __name__ == '__main__':
    +    # 执行用例
    +    seldom.main("./test_dir")
    +

    通过上面的配置,前置、后置动作就可以在平台上运行,当然,这样设置本地也可正常运行。

    + diff --git a/version/CHANGES.html b/version/CHANGES.html index 05a863b..9317b34 100644 --- a/version/CHANGES.html +++ b/version/CHANGES.html @@ -24,10 +24,10 @@ 版本更新 | seldom文档 - + -

    版本更新

    seldom 3.x v3

    3.10.0(2024-11-11)

    • 重要:所有app/web元素定位支持selector模式,详细查看文档。
    • 更新:sleep()增加默认值1s,也支持随机休眠范围:self.seep((1, 3))
    • 更新: appium_lab模块的 Action() 类下面的方法支持自定义休眠时间、间隔时间等。
    • 修复:Steps()类的 open() 方法默认传url报错 #241open in new window
    • 告警:type_enter()添加移除警告,推荐使用type()
    • 文档:
      • 修改playwright使用示例。
      • 增加pyAutoGUI使用示例。

    3.9.1(2024-10-10)

    • 更新:脚手架项目模板,增加run.py文件。
    • 修复:生成随机数,获取在线时间接口错误。
    • 修复:datetime.utcnow()在Python 3.12 告警。
    • App测试:
      • 修复back()home()方法报错。
      • 增加long_press_key()方法。
    • API测试:
      • 增加assertStatusOk()断言方法,断言接口返回状态码200
      • @check_response()装饰器重命名@api(),更简洁。
    • Web测试:
    • 文档更新:
    • 升级:XTestRunner==1.8.0

    3.9.0(2024-09-09)

    • App测试。
      • 升级Appium-Python-Client==4.1.0
      • 提供UiAutomator2OptionsEspressoOptions类,替换appium提供的这个两个类。
      • 移除不再支持的API: launch_app()close_app()reset()
      • 增加App相关操作时的日志。
    • Web测试浏览启动重构。
      • 支持start/end启动和关闭浏览器。
      • 支持start_class/end_class启动和关闭浏览器。
      • 支持new_browser()重新打开一个浏览器。
      • self.open() 检测到没有指定浏览器,不再默认启动一个Chrome()浏览器。
      • 链式API Steps()类添加browser参数。
    • Seldom.driver对象支持多线程。
    • log日志显示当前运行的线程。
    • Cache缓存类支持多线程。
    • 其他:移除直接依赖库:requestswebsocket-client, 使用间接依赖。
      • XTestRunner -> requests
      • Appium-Python-Client -> selenium -> websocket-client

    3.8.1(2024-08-20)

    • App测试。
      • 支持Appium-Python-Client==4.0.1,修复4.0.0 引起的问题。
    • seldom 命令,创建项目命令区分web/app/api项目。
    • 修复seldom-platform平台运行错误。

    3.8.0(2024-07-06)

    • API测试:
      • 支持执行Excel测试用例, seldom --api-excel api_case.xlsx 具体用法查看文档。
    • App:
      • 增加 self.keyboard_search()模拟键盘上的搜索按键。
    • 优化: @file_data()参数化装饰器代码。

    3.7.1(2024-06-01)

    • 优化:main() 中的path参数支持列表,可以指定多个目录或文件。
    • 新增:提供from seldom.utils.send_extend import RunResult 获取用例的执行数据。
    • App测试。
      • 增加swipe_right()左滑 和 swipe_left()右滑支持。
      • AppiumLab() 默认允许不传driver参数。
    • 其他:
      • Python 3.12 测试通过。

    3.7.0(2024-05-06)

    • @data()数据驱动装饰器增加cartesian=True参数,支持笛卡尔积。
    • 新增WebSocket接口测试支持。
    • App测试。
      • 支持Appium-Python-Client==4.0.0,修复4.0.0 引起的问题。
    • Web测试
      • 重新支持指定浏览器驱动,使用executable_path参数。
    • 其他:
      • 基于selenium依赖库,移除 Python 3.7 支持。

    3.6.0(2024-03-04)

    • seldom.main()方法增加failfast参数,debug模式,允许第一条用例失败,停止执行。
    • 增加@retry()装饰器,用于函数&方法错误重试。
    • HTTP测试
      • 支持swagger文档转seldom用例,使用命令 seldom -s2c swagger.json
      • 文档:增加 API Object model 概念的介绍,以及在seldom中的应用。

    3.5.0(2024-01-14)

    • 新增:支持 Postgre SQL 数据库操作。
    • web测试
      • pause() 用于暂停操作。
      • 移除webdriver_manager_extend.py文件(之前漏移除文件)。
    • App测试
      • 支持appium 2.0 正式版。
      • 支持appium-OCR-plugin插件。
      • 增加click_image()方法,支持图片点击定位。
      • press_key() 支持ENTER参数,模拟键盘回车。

    3.4.1(2023-11-26)

    • 修复:diff_json() 对比特殊数据的异常没有捕捉到。
    • setUpClass()/tearDownClass() 增加异常捕捉,避免报错之后,用例无法统计的问题。
    • web测试
      • screenshots() 增加images参数,支持传入截图对象 #202open in new window
      • open_electron() 增加chromedriver_path参数,支持手动指定驱动地址。

    3.4.0(2023-11-18)

    • 新增:dependent_func()装饰器,支持用例方法依赖调用,具体使用参考文档。
    • api测试
      • 修复:har2case 请求头参数类型判断不准的问题。
    • web测试
      • 增加open_electron() 方法,支持启动桌面electron应用。
      • 键盘操作Key()支持链式调用,例如: self.Keys(id_="kw").select_all().cut() 全选并删除。
    • cache操作日志增加 emoji。
    • 修复:diff_json() 优化,支持dict深度排序。 #197open in new window

    3.3.0(2023-09-26)

    • web测试
      • 浏览器驱动webdriver-manager 替换为selenium-manager
      • 增加execute_cdp_cmd() 方法。
    • 随机数据
      • online_timestamp() 在线获取时间戳。
      • online_now_datetime() 在线获取当前时间,格式为:%Y-%m-%d %H:%M:%S
    • 增加运行时内嵌(built-in)方法:base_url()driver() - 无需导入,可以在自动化程序任意位置使用这两个方法。
    • 移除parameterized 库的依赖,改为内置。
    • 修复:diff_json() 对比 [{}] 数据时报错。 #197open in new window

    3.2.3(2023-07-30)

    • HTTP自动化
      • confrun.py 支持 mock_url hook 钩子函数。
      • 增加 self.base_url 获取 base_url
    • Web自动化
      • 更新:get_elements() 增加empty参数,设置为True, 允许返回空列表 []
      • 更新: debug=True 模式,移除操作元素边框高亮,提高用例执行速度。
    • App测试
      • 修复:key_text() 无法输入点号.的问题。
    • 优化:seldom_log.log 文件只记录一次运行结果,减少文件大小。
    • 升级:webdriver_manager==4.0.0 #189open in new window
    • 其他: 添加 pyproject.toml 支持。
    • 文档:增加其他库的使用例子。

    3.2.2(2023-05-10)

    • 功能:增加@threads()支持多线程运行用例。
    • 功能:增加@rerun() 重复执行某个测试方法。
    • 功能:数据库操作
      • MySQLDB()MSSQLDB() 支持charset 参数设置字符集。
      • init_table() 批量插入数据库增加clear 参数,可以选择是否删除表再插入。
    • 功能:Web自动化
      • 新增save_screenshot() 截图保存本地。
      • 修改screenshots() 自动截图保存到HTML报告,移除file_path 参数。
      • 修改element_screenshot() 元素截图保存到HTML报告,移除file_path 参数。
      • type() 方法增加 click 参数,针对app元素优化,app的输入框往往需要点击以下锁定光标再输入。
    • 修复:浏览器配置参数 option 更名为 options
    • 其他:增加 python3.11 支持。

    3.2.1(2023-04-14)

    • 功能:增加@disk_cache()@memory_cache() 缓存装饰器。
    • 功能:app测试,seldom支持本身API支持appium定位。
    • 功能:db操作,增加insert_get_last_id() 方法,插入数据并返回id。
    • 修复:@data_class() 必传input_values 参数问题。
    • 修复:设置log等级,HTML报告无法根据等级打印日志问题。

    3.2.0(2023-03-14)

    • Web UI测试,增加一组新的警告框 alert 操作。
      • self.alert.text
      • self.alert.accept()
      • self.alert.dismiss()
      • self.alert.send_keys("text")
    • App UI测试。
      • AppiumLab() 类增加 context() 方法获取当前上下文。
      • AppiumLab() 类增加 size() 当前窗口尺寸。
    • API 测试。
      • 增加self.patch() 请求方法。
      • 增加self.json_to_dict() 支持单引号JSON格式转字典。
    • cache 增加文件锁,防止多线程读写错误(Windows不支持 fcntl)
    • 支持 XTestRunner=>1.6.2 版本
      • XML格式的报告支持 rerun 重跑参数。
      • HTML 报告skip用例样式微调。
      • HTML 重跑只显示最后一次结果。
      • SMTP 发送报告增加 ssl 参数。
    • seldom.main() 方法 ⚠ 不兼容更新
      • 移除 save_last_run 参数。
      • browser 参数支持dict 格式, 所有和浏览器配置相关的有发生修改。 包括
        • 设置浏览器驱动地址。
        • 设置 headless 模式。
        • 设置 options 参数。
        • 设置 selenium grid 地址。

    3.1.3(2023-02-15)

    • 功能:file_data() 增加end_line 参数,对于csv/excel文件支持读取到第几行结束。#163open in new window
    • 优化:self.assertElement() 断言元素时间过长的问题。
    • 优化:self.assertJSON() 断言日志,区分告警和错误。
    • 移除:self.jresponse() 方法。

    3.1.2(internal)

    内部版本:移除了日志打印的 emoji 表情。

    • 功能:seldom.main() 方法 path 参数支持斜杠路径\(windows系统用\ 表示路径)。

    3.1.1(2023-01-03)

    • 功能:confrun.py 增加start_run()/end_run() 钩子函数,用于运行前/后相关配置。
    • 优化:@api_data() 装饰器增加 headers 参数。
    • 优化:assertJSON() 断言增加 exclude 参数,屏蔽检查的字段,例如 ["start_time", "token"]
    • 修复:rediscover() 查找用例bug。
    • 依赖:升级XTestRunner==1.5.0 支持飞书/微信发送消息。

    3.1.0(2022-12-15)

    • 功能:提供 confrun.py 运行配置文件,配合 seldom 命令使用。
    • 功能:Web测试,增加 self.get_log() 方法。
    • 升级:webdriver_manager==3.8.5 ,支持Mac M1芯片的浏览器驱动。#159open in new window
    • 修复:seldom-platform平台同步多个项目引起的Bug。#158open in new window
    • 修复:Web测试, self.close() 关闭浏览器Bug。

    3.0.1(2022-11-5)

    • 功能:支持 SQL Server 数据库支持,需要单独安装pymssql库。
    • 功能:http接口测试增加curl()方法,支持请求转 cURL
    • 功能:seldom 命令增加--log-level 参数,log类型:TRACE, DEBUG, INFO, SUCCESS, WARNING, ERROR 等。

    3.0.0(2022-10-31)

    • seldom 3.0 的核心是支持app测试,并且相关API已稳定,目的已达到,接下来将会在3.0基础上继续开发。
    • 功能:collect_cases() 支持 warning 参数。

    3.0.0beta2(2022-10-26)

    • 修复:
      • 接口测试: 接口返回文本r.text 中文乱码问题。#146open in new window
      • app测试:感谢 @986379041
        • install_app() 错误
        • close_app() 错误
    • 功能:
    • 其他:
      • seldom 运行用例,优化内存使用。

    3.0.0beta1(2022-10-03)

    • 支持App测试
      • 依赖Appium-Python-Client库。
      • main() 增加 app_info, app_server 参数。
      • 增加appium_lab 模块。
      • 增加AppDriver 类。
    • 优化:基于pylint检查分析工具 优化代码。
    • 其他:
      • 生成随机数,增加get_timestamp() 获取当前时间戳。
      • 数据库查询,增加query_one() 查询一条数据。

    seldom 2.x v2

    2.10.6/7(2022-09-07)

    • 功能:seldom命令重大更新,支持更多参数和功能。
    • 功能:@file_data() 当设置Seldom.env时支持更深一级遍历。
    • 修复:diff_json() 对比数据错误。

    2.10.4/5(2022-08-17)

    • 重构log日志打印。 @Yongchin
      • 彻底修复日志重复打印的问题。
      • 移除log.printf() 非标准日志类型。
    • 修复:
      • sender() 发送完邮件,seldom_log.log 文件无法删除的问题。
      • TestMainExtendrun_cases()按照用例的顺序执行。@luna-CY
      • 修复request 带上url= 参数时异常。 @986379041@qq.com
    • 依赖:webdriver_manager依赖升级到3.8.2
    • 移除:Opera 浏览器的支持,selenium 4 已经移除了对opera的单独驱动支持。

    2.10.3(2022-07-17)

    • 数据驱动:@data()@file_data() 优化用例名称和描述。
    • 增加Seldom.env环境配置变量,@file_data() 数据驱动装饰器支持环境变量。
    • 修复:Edge浏览器启动错误。
    • 修复:HTTP接口测试self.post()方法 data参数不是dict类型错误。
    • 平台化支持:优化用例收集,具体查看文档。

    2.10.2(2022-06-25)

    更新:移动模式列表更新,去掉旧设备,增加新设备 linkopen in new window

    • 功能:测试报告显示断言信息。
    • 功能:main() 通过open=False可以控制运行完测试 不自动化打开测试报告。
    • Web 测试:
      • 增加self.new_browser() 可以打开新的浏览器,但只能使用selenium 的 API
      • 增加switch_to_frame_parent 切换到上一级表单,#118open in new window
      • 优化assertNotElement 执行慢的情况 #120open in new window
    • HTTP 测试:
      • 优化:JSON日志进行格式化打印。

    2.10.1(2022-05-30)

    • 修复:seldom log 问题引起,错误信息无法在控制台打印。

    2.10.0 为了解决107open in new window 问题,我们经过反复的讨论和优化,甚至对相关库XTestRunner做了修改;以为完美解决了这个问题,没想到还是引起了一些严重的错误。为此,我们感到非常沮丧,退回到2.9.0的实现方案。请升级到2.10.1版本。

    2.10.0(2022-05-25)

    • seldom log功能:
      • 修复打印日志显示固定文件的问题 107open in new window
      • log方法变更:log.warn() -> log.warning()
    • 功能:提供了cache 类来模拟缓存。
    • 功能:@data() 装饰器支持 dict 格式。
    • 功能:self.jresponse() 方法设计不合理,给以废弃提示;可以使用self.jsonpath()/self.jmespath() 替代。
    • 优化:断言方法assertSchema()assertJSON()支持response传参。
    • 优化:@check_response() check检查失败打印response
    • 修复:webdriver_manager 没有设置上限版本,导致webdriver_manager>=3.6.x 报错; 如果使用的 seldom<=2.9 请重新安装webdriver_manager==3.5.2

    2.9.0(2022-04-30)

    • seldom log功能:
      • 开放seldom 的log能力,可以配置颜色(colorlog)格式(format)等级(level) 等。
      • 重新定义了seldom打印日志的格式。
      • 所有log统一记录到/reports/seldom_log.log文件,不再每次生成单独文件。
    • 功能:提供了@check_response() 装饰器,为接口封装提供强大的支持。
    • 功能:集成genson库,生成JsonSchema模板 100open in new window
    • 功能:增加assertInPath() 断言方法。
    • 功能:增加jmespath()方法,方便提取测试数据。
    • 优化:jresponse() 增加对jmespath 语法的支持。
    • 优化:支持self.get()/self.post()/self.put()/self.delete() 返回response对象。

    2.8.0(2022-04-16)

    • 功能:增加MongoDB 数据库操作 93open in new window
    • 功能:支持单个用例执行 94open in new window
    • 功能:sendmail() 增加delete参数,发送完邮件删除reports/ 目录下面的报告和日志文件 95open in new window
    • 功能:增加jsonpathjresponse() ,更容易查找json数据 96open in new window
    • 功能:创建项目脚手架增加api测试例子:seldom -project mypro
    • 其他: 全新的seldom在线文档:https://seldomqa.github.io/ ,感谢 @nickliya

    2.7.0(2022-03-26)

    • 功能:引入loguru 库用于打印日志(之前使用python默认logging总有一些重复打印或不打印的问题)。
    • 功能:web自动化增加一套方法链(method chaining)的API。
    • 功能:支持手动指定浏览器驱动路径。

    2.6.0(2022-03-18)

    • 移除:自带的HTMLTestRunner,HTML报告采用XTestRunner
    • 移除:对unittest-xml-reporting库的依赖,XML报告使用XTestRunner
    • 修改:SMTP类发送邮件方法 sender() -> sendmail(), 发送邮件样式采用XTestRunner
    • 增加:seldom.main()方法增加tester 参数,用于设置测试人员名字,默认Anonymous
    • 增加:seldom.main()方法增加language 参数,用于设置报告中英文en/zh-CN,默认en
    • 增加:发送钉钉功能。
    • 修改:接口测试 self.session -> self.Session()
    • 移除:接口测试 self.request() 方法移除(注:该方法原本不可用)。

    2.5.1(2022-02-19)

    • 功能:Http接口测试使用日志打印接口信息
    • 功能:Http接口测试打印json参数 83open in new window
    • 修复:Web UI测试self.Key() 无法定位元素的问题

    2.5.0(2022-01-30)

    • 功能:支持测试平台化。
    • 功能:utils 增加file类,获取当前文件目录更方便。
    • 修复:self.select() 操作下拉选择错误。
    • 修复:diff_json() 对比json文件错误。

    2.4.2(2022-01-18)

    • 功能:增强@file_data使用方式,json/yaml支持内嵌dict数据。

    2.4.1(2022-01-17)

    • 优化:HTTP接口测试增加cookies信息打印。
    • 优化:@file_data() 使用,支持指定目录。
    • 修复:visit() 方法默认浏览器没有自动安装浏览器驱动的问题。
    • 修复:query_sql() 执行SQL没有提交的问题。

    2.4.0(2022-01-02)

    • 适配selenium 4.0+ ,适配相关依赖库新版本。
    • 测试用例支持label标签分类。
    • 接口测试增加打印入参信息 79open in new window
    • EdgeChromium浏览器支持headless模式。
    • Web自动化测试增加元素截图self.element_screenshot()
    • 优化HTML测试报告样式。
    • 优化邮件模板样式。

    2.3.3(2021-11-12)

    2.3.2(2021-11-08)

    • 接口调用如果是图片类型,不在打印内容。
    • 增加screenshot 针对定位的元素截图, 用法self.screenshot(id="xx")
    • 测试报告:优化截图的样式。
    • 发邮件功能,默认增加附件为测试报告。

    2.3.1(2021-11-02)

    • 修复assertUrl()assertInUrl() 断言中文编码错误。
    • 增加文件路径操作。
      • file_path() 获取当前文件路径。
      • file_dir() 获取当前文件目录。
      • file_dir_dir() 获取当前文件目录的目录。
      • file_dir_dir_dir() 获取当前文件目录的目录的目录。
      • init_env_path() 添加路径到环境变量。
    • 优化main() 方法中代码的执行顺序。

    2.3.0(2021-10-18)

    • 集成 webdriver-manager,不需要再单独安装浏览器驱动。
    • seldom logo 显示版本号。
    • 固定selenium版本号,暂没做4.0.0适配。

    2.2.4(2021-09-21)

    • 修复HTTP接口测试,指定url参数错误的问题。71open in new window
    • 支持发送多人邮件。72open in new window
    • 优化HTMLTestRunner, 重跑次数不记录为用例数。
    • 修复pip安装缺少description.rst 问题。

    2.2.3(2021-08-27)

    2.2.2(2021-08-13)

    • 优化db操作方法。
    • 打印logs合并到 reports 目录。

    2.2.1(2021-06-30)

    • webdriver文件增加类型。
    • 删除utils 错误代码。
    • 修复:diff_json() 函数处理复杂数据报错 #66
    • 修复:运行接口测试用例报 driver 错误 #68
    • 修复:测试报告popper.min.js CDN 太慢的问题

    2.2.0(2021-06-15)

    • 增加接口测试方法sessionrequest
    • 增加seldom -h2c参数,用于将har文件转成测试用例。

    2.1.1(2021-05-28)

    • 增加随机生成时间方法get_past_time()get_future_time()
    • 优化:截图方法screenshots(),可以在任意位置使用该方法生成截图,并显示在HTML测试报告中。
    • 修复:接口测试main()中base_url 和 方法中的 url 同时存在的问题。
    • 修复:优化MySQL数据库连接的问题。
    • 修复:发送邮件时的错误。
    • 修复:当main()中的timeout设置为1时,断言失败的问题。

    2.1.0(2021-05-19)

    • 增加数据库操作,同时支持sqlite3mysql
    • 优化file_data(),兼容2.0.0用法。

    2.0.1(2021-05-07)

    • 优化 file_data(), 自动查找数据文件。
    • 优化脚手架,创建项目例子更新。

    2.0.0(2021-04-24)

    • webdriver API 修改
      • 移除 self.get()
      • 增加 self.visit()
      • 移除 self.open_new_window()
      • 移除 self.current_window_handle()
      • 移除 self.new_window_handle()
      • 移除 self.window_handles()
      • 修改 self.switch_to_window() 用法
      • 优化打印日志,为每种操作加上 emoji
    • 增加expected_failure用例装饰器,用于标记一条用例失败
    • 增加 file_dir(), 返回当前文件所在目录的绝对路径。
    • 运行完成自动通过浏览器打开HTML报告
    • main()方法修改
      • 修复debug参数类型错误异常提示
      • 控制台更换字符logo*
    • 整合 webdriver/request
    • 上线 readthedocs 文档

    2.0.0.beta(2021-03-24)

    • 支持 HTTP接口测试

    seldom 1.x v1

    1.10.3(2021-03-23)

    ...

    1.10.2(2021-03-13)

    • HTMLTestRunner代码优化
    • 修复bug

    1.10.1(2021-03-04)

    • webdriver代码重构
    • 修复严重bug

    1.10.0(2021-01-29)

    • 增加断言元素方法:assertElementassertNotElement
    • 增加单个测试类、用例执行的方法
    • 修复报告样式bug
    • 命令行工具优化

    1.9.0(2020-12-19)

    • 测试报告重构
      • 用例描述单独一列
      • 增加单个用例运行时间
      • 新的报告样式
    • 脚手架工具创建项目更新
    • 增加随机生成手机号方法

    1.8.0(2020-11-17)

    • 增加用例依赖装饰器

    1.7.2(2020-10-10)

    • bug修复版本

    1.7.0(2020-09-21)

    • 重构浏览器驱动,开放浏览器可配置能力。

    1.6.0(2020-08-24)

    • 浏览器增加简写
    • 支持 logs 日志
    • 支持 XML 测试报告
    • 增加 file_data 方法实现参数化。
    • 修复一些bug

    1.5.6(2020-07-24)

    • 封装test fixture方法

    1.5.5(2020-06-29)

    • 修改HTMLTestRunner 错误日志的展示
    • 增加mobile web的支持

    1.5.4(2020-06-04)

    • 增加keys键盘操作
    • 元素操作增加聚焦
    • debug 模式增加慢操作

    1.5.3(2020-05-31)

    • 修复bug
    • 增加 yaml_to_list()方法

    1.5.2(202x-05-16)

    • 修复bug

    1.5.1 (2020-05-14)

    • 修复日志重复打印问题
    • 修复测试报告不截图问题
    • 日志增加emoji表情

    1.5.0(2020-04-29)

    • 自动化运行过程中,对操作的元素加边框,使其更醒目。
    • 去掉对 setUpClass()方法的占用,代码做了较大重构。
    • 在使用poium时,驱动的获取方式改变,这一点不向下兼容。

    1.2.6 (2020-04-22)

    • 完善自动化发邮件功能
    • 增加 type_enter() 方法
    • 优化项目的代码的调用
    • 修复 seldom + poium 日志问题

    1.2.5(2020-04-13)

    • 重新定制测试报告样式
    • seldom.main()增加timeout参数

    1.2.4(2020-03-19)

    • 增加数据解析相关操作方法
    • 增加跳过测试相关方法
    • 增加发邮件功能
    • 修复bug, 优化代码

    1.2.3(2020-03-11)

    • 增加 slow_click() 方法。
    • seldom.main() 默认运行当前文件不需要传参。
    • seldom.main(report="report-name.html") 允许自定义报告名称。

    1.2.2(2020-03-03)

    • fix bug
    • add function: csv_to_list()/ excel_to_list()

    1.2.0(2020-02-01)

    Global launch browser

    1.1.0(2020-01-19)

    selenium grid support Added safari support

    1.0.0(2020-01-04)

    The framework function has been basically improved. I'm glad to release version 1.0

    seldom 0.x

    0.3.6(2019-12-23)

    Add cookie manipulation APIs Optimized element wait

    0.3.5(2019-12-06)

    Added chrome/firefox browser driver download command Driver file path Settings are supported

    0.3.3(2019-11-30)

    add skip case

    0.3.2(2019-11-27)

    Added a switch to display the last rerun result Optimized assertion method

    0.3.0(2019-11-21)

    Update element positioning

    0.2.0(2019-11-17)

    Change the project name to seldom Introducing the poium test library,

    pyse

    0.1.5(2019-11-15)

    • Increased test case failure rerun
    • Add use case failure screenshots

    0.0.9(2018-03-29)

    Simplifying API calls

    0.0.8(2017-11-23)

    add parameterized Beautification test report

    0.0.7(2016-11-09)

    Re based on unittest.

    0.0.6(2016-04-29)

    add setup.py file, Specification of the installation process, a time to install all dependencies. Delete unnecessary files

    0.0.5

    Increase the support of multiple positioning methods

    0.0.4

    Method to add default to wait. Modify the realization of the individual methods

    0.0.3.1 version update

    • Repair part bug.

    0.0.3 version update(2015-09-08)

    • With the nose instead of unittest.
    • Discard HTMLTestRunner,Integrated nose-html-reporting.
    • modify the examples under demo.

    0.0.2 version update(2015-09-08)

    • all the elements of the operation selector xpath replaced by css, css syntax because more concise.
    • when you run the test case no longer need to specify the directory, the default directory for the current test.
    • modify the examples under demo.
    - +

    版本更新

    seldom 3.x v3

    3.11.0(2024-12-19)

    • 功能:平台化用例执行,seldom.main()支持加载confrun.py中的 start_run()/end_run()
    • 功能:平台化用例解析,识别用例标签label
    • HTTP测试:通过confrun.py支持proxies()配置全局的请求代理。
    • App测试:
      • appium_lab 增加 drag_from_to()方法,支持坐标位滑动。感谢@guweifan
      • appium_lab 增加AppiumService类,支持启动appium server。感谢@guweifan
    • 优化:jsonpath.py的代码。

    3.10.0(2024-11-11)

    • 重要:所有app/web元素定位支持selector模式,详细查看文档。
    • 更新:sleep()增加默认值1s,也支持随机休眠范围:self.seep((1, 3))
    • 更新: appium_lab模块的 Action() 类下面的方法支持自定义休眠时间、间隔时间等。
    • 修复:Steps()类的 open() 方法默认传url报错 #241open in new window
    • 告警:type_enter()添加移除警告,推荐使用type()
    • 文档:
      • 修改playwright使用示例。
      • 增加pyAutoGUI使用示例。

    3.9.1(2024-10-10)

    • 更新:脚手架项目模板,增加run.py文件。
    • 修复:生成随机数,获取在线时间接口错误。
    • 修复:datetime.utcnow()在Python 3.12 告警。
    • App测试:
      • 修复back()home()方法报错。
      • 增加long_press_key()方法。
    • API测试:
      • 增加assertStatusOk()断言方法,断言接口返回状态码200
      • @check_response()装饰器重命名@api(),更简洁。
    • Web测试:
    • 文档更新:
    • 升级:XTestRunner==1.8.0

    3.9.0(2024-09-09)

    • App测试。
      • 升级Appium-Python-Client==4.1.0
      • 提供UiAutomator2OptionsEspressoOptions类,替换appium提供的这个两个类。
      • 移除不再支持的API: launch_app()close_app()reset()
      • 增加App相关操作时的日志。
    • Web测试浏览启动重构。
      • 支持start/end启动和关闭浏览器。
      • 支持start_class/end_class启动和关闭浏览器。
      • 支持new_browser()重新打开一个浏览器。
      • self.open() 检测到没有指定浏览器,不再默认启动一个Chrome()浏览器。
      • 链式API Steps()类添加browser参数。
    • Seldom.driver对象支持多线程。
    • log日志显示当前运行的线程。
    • Cache缓存类支持多线程。
    • 其他:移除直接依赖库:requestswebsocket-client, 使用间接依赖。
      • XTestRunner -> requests
      • Appium-Python-Client -> selenium -> websocket-client

    3.8.1(2024-08-20)

    • App测试。
      • 支持Appium-Python-Client==4.0.1,修复4.0.0 引起的问题。
    • seldom 命令,创建项目命令区分web/app/api项目。
    • 修复seldom-platform平台运行错误。

    3.8.0(2024-07-06)

    • API测试:
      • 支持执行Excel测试用例, seldom --api-excel api_case.xlsx 具体用法查看文档。
    • App:
      • 增加 self.keyboard_search()模拟键盘上的搜索按键。
    • 优化: @file_data()参数化装饰器代码。

    3.7.1(2024-06-01)

    • 优化:main() 中的path参数支持列表,可以指定多个目录或文件。
    • 新增:提供from seldom.utils.send_extend import RunResult 获取用例的执行数据。
    • App测试。
      • 增加swipe_right()左滑 和 swipe_left()右滑支持。
      • AppiumLab() 默认允许不传driver参数。
    • 其他:
      • Python 3.12 测试通过。

    3.7.0(2024-05-06)

    • @data()数据驱动装饰器增加cartesian=True参数,支持笛卡尔积。
    • 新增WebSocket接口测试支持。
    • App测试。
      • 支持Appium-Python-Client==4.0.0,修复4.0.0 引起的问题。
    • Web测试
      • 重新支持指定浏览器驱动,使用executable_path参数。
    • 其他:
      • 基于selenium依赖库,移除 Python 3.7 支持。

    3.6.0(2024-03-04)

    • seldom.main()方法增加failfast参数,debug模式,允许第一条用例失败,停止执行。
    • 增加@retry()装饰器,用于函数&方法错误重试。
    • HTTP测试
      • 支持swagger文档转seldom用例,使用命令 seldom -s2c swagger.json
      • 文档:增加 API Object model 概念的介绍,以及在seldom中的应用。

    3.5.0(2024-01-14)

    • 新增:支持 Postgre SQL 数据库操作。
    • web测试
      • pause() 用于暂停操作。
      • 移除webdriver_manager_extend.py文件(之前漏移除文件)。
    • App测试
      • 支持appium 2.0 正式版。
      • 支持appium-OCR-plugin插件。
      • 增加click_image()方法,支持图片点击定位。
      • press_key() 支持ENTER参数,模拟键盘回车。

    3.4.1(2023-11-26)

    • 修复:diff_json() 对比特殊数据的异常没有捕捉到。
    • setUpClass()/tearDownClass() 增加异常捕捉,避免报错之后,用例无法统计的问题。
    • web测试
      • screenshots() 增加images参数,支持传入截图对象 #202open in new window
      • open_electron() 增加chromedriver_path参数,支持手动指定驱动地址。

    3.4.0(2023-11-18)

    • 新增:dependent_func()装饰器,支持用例方法依赖调用,具体使用参考文档。
    • api测试
      • 修复:har2case 请求头参数类型判断不准的问题。
    • web测试
      • 增加open_electron() 方法,支持启动桌面electron应用。
      • 键盘操作Key()支持链式调用,例如: self.Keys(id_="kw").select_all().cut() 全选并删除。
    • cache操作日志增加 emoji。
    • 修复:diff_json() 优化,支持dict深度排序。 #197open in new window

    3.3.0(2023-09-26)

    • web测试
      • 浏览器驱动webdriver-manager 替换为selenium-manager
      • 增加execute_cdp_cmd() 方法。
    • 随机数据
      • online_timestamp() 在线获取时间戳。
      • online_now_datetime() 在线获取当前时间,格式为:%Y-%m-%d %H:%M:%S
    • 增加运行时内嵌(built-in)方法:base_url()driver() - 无需导入,可以在自动化程序任意位置使用这两个方法。
    • 移除parameterized 库的依赖,改为内置。
    • 修复:diff_json() 对比 [{}] 数据时报错。 #197open in new window

    3.2.3(2023-07-30)

    • HTTP自动化
      • confrun.py 支持 mock_url hook 钩子函数。
      • 增加 self.base_url 获取 base_url
    • Web自动化
      • 更新:get_elements() 增加empty参数,设置为True, 允许返回空列表 []
      • 更新: debug=True 模式,移除操作元素边框高亮,提高用例执行速度。
    • App测试
      • 修复:key_text() 无法输入点号.的问题。
    • 优化:seldom_log.log 文件只记录一次运行结果,减少文件大小。
    • 升级:webdriver_manager==4.0.0 #189open in new window
    • 其他: 添加 pyproject.toml 支持。
    • 文档:增加其他库的使用例子。

    3.2.2(2023-05-10)

    • 功能:增加@threads()支持多线程运行用例。
    • 功能:增加@rerun() 重复执行某个测试方法。
    • 功能:数据库操作
      • MySQLDB()MSSQLDB() 支持charset 参数设置字符集。
      • init_table() 批量插入数据库增加clear 参数,可以选择是否删除表再插入。
    • 功能:Web自动化
      • 新增save_screenshot() 截图保存本地。
      • 修改screenshots() 自动截图保存到HTML报告,移除file_path 参数。
      • 修改element_screenshot() 元素截图保存到HTML报告,移除file_path 参数。
      • type() 方法增加 click 参数,针对app元素优化,app的输入框往往需要点击以下锁定光标再输入。
    • 修复:浏览器配置参数 option 更名为 options
    • 其他:增加 python3.11 支持。

    3.2.1(2023-04-14)

    • 功能:增加@disk_cache()@memory_cache() 缓存装饰器。
    • 功能:app测试,seldom支持本身API支持appium定位。
    • 功能:db操作,增加insert_get_last_id() 方法,插入数据并返回id。
    • 修复:@data_class() 必传input_values 参数问题。
    • 修复:设置log等级,HTML报告无法根据等级打印日志问题。

    3.2.0(2023-03-14)

    • Web UI测试,增加一组新的警告框 alert 操作。
      • self.alert.text
      • self.alert.accept()
      • self.alert.dismiss()
      • self.alert.send_keys("text")
    • App UI测试。
      • AppiumLab() 类增加 context() 方法获取当前上下文。
      • AppiumLab() 类增加 size() 当前窗口尺寸。
    • API 测试。
      • 增加self.patch() 请求方法。
      • 增加self.json_to_dict() 支持单引号JSON格式转字典。
    • cache 增加文件锁,防止多线程读写错误(Windows不支持 fcntl)
    • 支持 XTestRunner=>1.6.2 版本
      • XML格式的报告支持 rerun 重跑参数。
      • HTML 报告skip用例样式微调。
      • HTML 重跑只显示最后一次结果。
      • SMTP 发送报告增加 ssl 参数。
    • seldom.main() 方法 ⚠ 不兼容更新
      • 移除 save_last_run 参数。
      • browser 参数支持dict 格式, 所有和浏览器配置相关的有发生修改。 包括
        • 设置浏览器驱动地址。
        • 设置 headless 模式。
        • 设置 options 参数。
        • 设置 selenium grid 地址。

    3.1.3(2023-02-15)

    • 功能:file_data() 增加end_line 参数,对于csv/excel文件支持读取到第几行结束。#163open in new window
    • 优化:self.assertElement() 断言元素时间过长的问题。
    • 优化:self.assertJSON() 断言日志,区分告警和错误。
    • 移除:self.jresponse() 方法。

    3.1.2(internal)

    内部版本:移除了日志打印的 emoji 表情。

    • 功能:seldom.main() 方法 path 参数支持斜杠路径\(windows系统用\ 表示路径)。

    3.1.1(2023-01-03)

    • 功能:confrun.py 增加start_run()/end_run() 钩子函数,用于运行前/后相关配置。
    • 优化:@api_data() 装饰器增加 headers 参数。
    • 优化:assertJSON() 断言增加 exclude 参数,屏蔽检查的字段,例如 ["start_time", "token"]
    • 修复:rediscover() 查找用例bug。
    • 依赖:升级XTestRunner==1.5.0 支持飞书/微信发送消息。

    3.1.0(2022-12-15)

    • 功能:提供 confrun.py 运行配置文件,配合 seldom 命令使用。
    • 功能:Web测试,增加 self.get_log() 方法。
    • 升级:webdriver_manager==3.8.5 ,支持Mac M1芯片的浏览器驱动。#159open in new window
    • 修复:seldom-platform平台同步多个项目引起的Bug。#158open in new window
    • 修复:Web测试, self.close() 关闭浏览器Bug。

    3.0.1(2022-11-5)

    • 功能:支持 SQL Server 数据库支持,需要单独安装pymssql库。
    • 功能:http接口测试增加curl()方法,支持请求转 cURL
    • 功能:seldom 命令增加--log-level 参数,log类型:TRACE, DEBUG, INFO, SUCCESS, WARNING, ERROR 等。

    3.0.0(2022-10-31)

    • seldom 3.0 的核心是支持app测试,并且相关API已稳定,目的已达到,接下来将会在3.0基础上继续开发。
    • 功能:collect_cases() 支持 warning 参数。

    3.0.0beta2(2022-10-26)

    • 修复:
      • 接口测试: 接口返回文本r.text 中文乱码问题。#146open in new window
      • app测试:感谢 @986379041
        • install_app() 错误
        • close_app() 错误
    • 功能:
    • 其他:
      • seldom 运行用例,优化内存使用。

    3.0.0beta1(2022-10-03)

    • 支持App测试
      • 依赖Appium-Python-Client库。
      • main() 增加 app_info, app_server 参数。
      • 增加appium_lab 模块。
      • 增加AppDriver 类。
    • 优化:基于pylint检查分析工具 优化代码。
    • 其他:
      • 生成随机数,增加get_timestamp() 获取当前时间戳。
      • 数据库查询,增加query_one() 查询一条数据。

    seldom 2.x v2

    2.10.6/7(2022-09-07)

    • 功能:seldom命令重大更新,支持更多参数和功能。
    • 功能:@file_data() 当设置Seldom.env时支持更深一级遍历。
    • 修复:diff_json() 对比数据错误。

    2.10.4/5(2022-08-17)

    • 重构log日志打印。 @Yongchin
      • 彻底修复日志重复打印的问题。
      • 移除log.printf() 非标准日志类型。
    • 修复:
      • sender() 发送完邮件,seldom_log.log 文件无法删除的问题。
      • TestMainExtendrun_cases()按照用例的顺序执行。@luna-CY
      • 修复request 带上url= 参数时异常。 @986379041@qq.com
    • 依赖:webdriver_manager依赖升级到3.8.2
    • 移除:Opera 浏览器的支持,selenium 4 已经移除了对opera的单独驱动支持。

    2.10.3(2022-07-17)

    • 数据驱动:@data()@file_data() 优化用例名称和描述。
    • 增加Seldom.env环境配置变量,@file_data() 数据驱动装饰器支持环境变量。
    • 修复:Edge浏览器启动错误。
    • 修复:HTTP接口测试self.post()方法 data参数不是dict类型错误。
    • 平台化支持:优化用例收集,具体查看文档。

    2.10.2(2022-06-25)

    更新:移动模式列表更新,去掉旧设备,增加新设备 linkopen in new window

    • 功能:测试报告显示断言信息。
    • 功能:main() 通过open=False可以控制运行完测试 不自动化打开测试报告。
    • Web 测试:
      • 增加self.new_browser() 可以打开新的浏览器,但只能使用selenium 的 API
      • 增加switch_to_frame_parent 切换到上一级表单,#118open in new window
      • 优化assertNotElement 执行慢的情况 #120open in new window
    • HTTP 测试:
      • 优化:JSON日志进行格式化打印。

    2.10.1(2022-05-30)

    • 修复:seldom log 问题引起,错误信息无法在控制台打印。

    2.10.0 为了解决107open in new window 问题,我们经过反复的讨论和优化,甚至对相关库XTestRunner做了修改;以为完美解决了这个问题,没想到还是引起了一些严重的错误。为此,我们感到非常沮丧,退回到2.9.0的实现方案。请升级到2.10.1版本。

    2.10.0(2022-05-25)

    • seldom log功能:
      • 修复打印日志显示固定文件的问题 107open in new window
      • log方法变更:log.warn() -> log.warning()
    • 功能:提供了cache 类来模拟缓存。
    • 功能:@data() 装饰器支持 dict 格式。
    • 功能:self.jresponse() 方法设计不合理,给以废弃提示;可以使用self.jsonpath()/self.jmespath() 替代。
    • 优化:断言方法assertSchema()assertJSON()支持response传参。
    • 优化:@check_response() check检查失败打印response
    • 修复:webdriver_manager 没有设置上限版本,导致webdriver_manager>=3.6.x 报错; 如果使用的 seldom<=2.9 请重新安装webdriver_manager==3.5.2

    2.9.0(2022-04-30)

    • seldom log功能:
      • 开放seldom 的log能力,可以配置颜色(colorlog)格式(format)等级(level) 等。
      • 重新定义了seldom打印日志的格式。
      • 所有log统一记录到/reports/seldom_log.log文件,不再每次生成单独文件。
    • 功能:提供了@check_response() 装饰器,为接口封装提供强大的支持。
    • 功能:集成genson库,生成JsonSchema模板 100open in new window
    • 功能:增加assertInPath() 断言方法。
    • 功能:增加jmespath()方法,方便提取测试数据。
    • 优化:jresponse() 增加对jmespath 语法的支持。
    • 优化:支持self.get()/self.post()/self.put()/self.delete() 返回response对象。

    2.8.0(2022-04-16)

    • 功能:增加MongoDB 数据库操作 93open in new window
    • 功能:支持单个用例执行 94open in new window
    • 功能:sendmail() 增加delete参数,发送完邮件删除reports/ 目录下面的报告和日志文件 95open in new window
    • 功能:增加jsonpathjresponse() ,更容易查找json数据 96open in new window
    • 功能:创建项目脚手架增加api测试例子:seldom -project mypro
    • 其他: 全新的seldom在线文档:https://seldomqa.github.io/ ,感谢 @nickliya

    2.7.0(2022-03-26)

    • 功能:引入loguru 库用于打印日志(之前使用python默认logging总有一些重复打印或不打印的问题)。
    • 功能:web自动化增加一套方法链(method chaining)的API。
    • 功能:支持手动指定浏览器驱动路径。

    2.6.0(2022-03-18)

    • 移除:自带的HTMLTestRunner,HTML报告采用XTestRunner
    • 移除:对unittest-xml-reporting库的依赖,XML报告使用XTestRunner
    • 修改:SMTP类发送邮件方法 sender() -> sendmail(), 发送邮件样式采用XTestRunner
    • 增加:seldom.main()方法增加tester 参数,用于设置测试人员名字,默认Anonymous
    • 增加:seldom.main()方法增加language 参数,用于设置报告中英文en/zh-CN,默认en
    • 增加:发送钉钉功能。
    • 修改:接口测试 self.session -> self.Session()
    • 移除:接口测试 self.request() 方法移除(注:该方法原本不可用)。

    2.5.1(2022-02-19)

    • 功能:Http接口测试使用日志打印接口信息
    • 功能:Http接口测试打印json参数 83open in new window
    • 修复:Web UI测试self.Key() 无法定位元素的问题

    2.5.0(2022-01-30)

    • 功能:支持测试平台化。
    • 功能:utils 增加file类,获取当前文件目录更方便。
    • 修复:self.select() 操作下拉选择错误。
    • 修复:diff_json() 对比json文件错误。

    2.4.2(2022-01-18)

    • 功能:增强@file_data使用方式,json/yaml支持内嵌dict数据。

    2.4.1(2022-01-17)

    • 优化:HTTP接口测试增加cookies信息打印。
    • 优化:@file_data() 使用,支持指定目录。
    • 修复:visit() 方法默认浏览器没有自动安装浏览器驱动的问题。
    • 修复:query_sql() 执行SQL没有提交的问题。

    2.4.0(2022-01-02)

    • 适配selenium 4.0+ ,适配相关依赖库新版本。
    • 测试用例支持label标签分类。
    • 接口测试增加打印入参信息 79open in new window
    • EdgeChromium浏览器支持headless模式。
    • Web自动化测试增加元素截图self.element_screenshot()
    • 优化HTML测试报告样式。
    • 优化邮件模板样式。

    2.3.3(2021-11-12)

    2.3.2(2021-11-08)

    • 接口调用如果是图片类型,不在打印内容。
    • 增加screenshot 针对定位的元素截图, 用法self.screenshot(id="xx")
    • 测试报告:优化截图的样式。
    • 发邮件功能,默认增加附件为测试报告。

    2.3.1(2021-11-02)

    • 修复assertUrl()assertInUrl() 断言中文编码错误。
    • 增加文件路径操作。
      • file_path() 获取当前文件路径。
      • file_dir() 获取当前文件目录。
      • file_dir_dir() 获取当前文件目录的目录。
      • file_dir_dir_dir() 获取当前文件目录的目录的目录。
      • init_env_path() 添加路径到环境变量。
    • 优化main() 方法中代码的执行顺序。

    2.3.0(2021-10-18)

    • 集成 webdriver-manager,不需要再单独安装浏览器驱动。
    • seldom logo 显示版本号。
    • 固定selenium版本号,暂没做4.0.0适配。

    2.2.4(2021-09-21)

    • 修复HTTP接口测试,指定url参数错误的问题。71open in new window
    • 支持发送多人邮件。72open in new window
    • 优化HTMLTestRunner, 重跑次数不记录为用例数。
    • 修复pip安装缺少description.rst 问题。

    2.2.3(2021-08-27)

    2.2.2(2021-08-13)

    • 优化db操作方法。
    • 打印logs合并到 reports 目录。

    2.2.1(2021-06-30)

    • webdriver文件增加类型。
    • 删除utils 错误代码。
    • 修复:diff_json() 函数处理复杂数据报错 #66
    • 修复:运行接口测试用例报 driver 错误 #68
    • 修复:测试报告popper.min.js CDN 太慢的问题

    2.2.0(2021-06-15)

    • 增加接口测试方法sessionrequest
    • 增加seldom -h2c参数,用于将har文件转成测试用例。

    2.1.1(2021-05-28)

    • 增加随机生成时间方法get_past_time()get_future_time()
    • 优化:截图方法screenshots(),可以在任意位置使用该方法生成截图,并显示在HTML测试报告中。
    • 修复:接口测试main()中base_url 和 方法中的 url 同时存在的问题。
    • 修复:优化MySQL数据库连接的问题。
    • 修复:发送邮件时的错误。
    • 修复:当main()中的timeout设置为1时,断言失败的问题。

    2.1.0(2021-05-19)

    • 增加数据库操作,同时支持sqlite3mysql
    • 优化file_data(),兼容2.0.0用法。

    2.0.1(2021-05-07)

    • 优化 file_data(), 自动查找数据文件。
    • 优化脚手架,创建项目例子更新。

    2.0.0(2021-04-24)

    • webdriver API 修改
      • 移除 self.get()
      • 增加 self.visit()
      • 移除 self.open_new_window()
      • 移除 self.current_window_handle()
      • 移除 self.new_window_handle()
      • 移除 self.window_handles()
      • 修改 self.switch_to_window() 用法
      • 优化打印日志,为每种操作加上 emoji
    • 增加expected_failure用例装饰器,用于标记一条用例失败
    • 增加 file_dir(), 返回当前文件所在目录的绝对路径。
    • 运行完成自动通过浏览器打开HTML报告
    • main()方法修改
      • 修复debug参数类型错误异常提示
      • 控制台更换字符logo*
    • 整合 webdriver/request
    • 上线 readthedocs 文档

    2.0.0.beta(2021-03-24)

    • 支持 HTTP接口测试

    seldom 1.x v1

    1.10.3(2021-03-23)

    ...

    1.10.2(2021-03-13)

    • HTMLTestRunner代码优化
    • 修复bug

    1.10.1(2021-03-04)

    • webdriver代码重构
    • 修复严重bug

    1.10.0(2021-01-29)

    • 增加断言元素方法:assertElementassertNotElement
    • 增加单个测试类、用例执行的方法
    • 修复报告样式bug
    • 命令行工具优化

    1.9.0(2020-12-19)

    • 测试报告重构
      • 用例描述单独一列
      • 增加单个用例运行时间
      • 新的报告样式
    • 脚手架工具创建项目更新
    • 增加随机生成手机号方法

    1.8.0(2020-11-17)

    • 增加用例依赖装饰器

    1.7.2(2020-10-10)

    • bug修复版本

    1.7.0(2020-09-21)

    • 重构浏览器驱动,开放浏览器可配置能力。

    1.6.0(2020-08-24)

    • 浏览器增加简写
    • 支持 logs 日志
    • 支持 XML 测试报告
    • 增加 file_data 方法实现参数化。
    • 修复一些bug

    1.5.6(2020-07-24)

    • 封装test fixture方法

    1.5.5(2020-06-29)

    • 修改HTMLTestRunner 错误日志的展示
    • 增加mobile web的支持

    1.5.4(2020-06-04)

    • 增加keys键盘操作
    • 元素操作增加聚焦
    • debug 模式增加慢操作

    1.5.3(2020-05-31)

    • 修复bug
    • 增加 yaml_to_list()方法

    1.5.2(202x-05-16)

    • 修复bug

    1.5.1 (2020-05-14)

    • 修复日志重复打印问题
    • 修复测试报告不截图问题
    • 日志增加emoji表情

    1.5.0(2020-04-29)

    • 自动化运行过程中,对操作的元素加边框,使其更醒目。
    • 去掉对 setUpClass()方法的占用,代码做了较大重构。
    • 在使用poium时,驱动的获取方式改变,这一点不向下兼容。

    1.2.6 (2020-04-22)

    • 完善自动化发邮件功能
    • 增加 type_enter() 方法
    • 优化项目的代码的调用
    • 修复 seldom + poium 日志问题

    1.2.5(2020-04-13)

    • 重新定制测试报告样式
    • seldom.main()增加timeout参数

    1.2.4(2020-03-19)

    • 增加数据解析相关操作方法
    • 增加跳过测试相关方法
    • 增加发邮件功能
    • 修复bug, 优化代码

    1.2.3(2020-03-11)

    • 增加 slow_click() 方法。
    • seldom.main() 默认运行当前文件不需要传参。
    • seldom.main(report="report-name.html") 允许自定义报告名称。

    1.2.2(2020-03-03)

    • fix bug
    • add function: csv_to_list()/ excel_to_list()

    1.2.0(2020-02-01)

    Global launch browser

    1.1.0(2020-01-19)

    selenium grid support Added safari support

    1.0.0(2020-01-04)

    The framework function has been basically improved. I'm glad to release version 1.0

    seldom 0.x

    0.3.6(2019-12-23)

    Add cookie manipulation APIs Optimized element wait

    0.3.5(2019-12-06)

    Added chrome/firefox browser driver download command Driver file path Settings are supported

    0.3.3(2019-11-30)

    add skip case

    0.3.2(2019-11-27)

    Added a switch to display the last rerun result Optimized assertion method

    0.3.0(2019-11-21)

    Update element positioning

    0.2.0(2019-11-17)

    Change the project name to seldom Introducing the poium test library,

    pyse

    0.1.5(2019-11-15)

    • Increased test case failure rerun
    • Add use case failure screenshots

    0.0.9(2018-03-29)

    Simplifying API calls

    0.0.8(2017-11-23)

    add parameterized Beautification test report

    0.0.7(2016-11-09)

    Re based on unittest.

    0.0.6(2016-04-29)

    add setup.py file, Specification of the installation process, a time to install all dependencies. Delete unnecessary files

    0.0.5

    Increase the support of multiple positioning methods

    0.0.4

    Method to add default to wait. Modify the realization of the individual methods

    0.0.3.1 version update

    • Repair part bug.

    0.0.3 version update(2015-09-08)

    • With the nose instead of unittest.
    • Discard HTMLTestRunner,Integrated nose-html-reporting.
    • modify the examples under demo.

    0.0.2 version update(2015-09-08)

    • all the elements of the operation selector xpath replaced by css, css syntax because more concise.
    • when you run the test case no longer need to specify the directory, the default directory for the current test.
    • modify the examples under demo.
    + diff --git a/web-testing/browser_driver.html b/web-testing/browser_driver.html index 272635f..2fe01db 100644 --- a/web-testing/browser_driver.html +++ b/web-testing/browser_driver.html @@ -24,7 +24,7 @@ 浏览器与驱动 | seldom文档 - +

    浏览器与驱动

    管理浏览器驱动

    seldom 2.3.0 版本集成webdriver_manager管理浏览器驱动。

    seldom 3.3.0 版本移除了webdriver_manager,selenium 4.6 之后内置了 selenium-manager 可以自动管理浏览器驱动。

    自动下载

    如果你不配置浏览器驱动也没关系,seldom(selenium)会根据你使用的浏览器版本,自动化下载对应的驱动文件。

    • 编写简单的用例
    import seldom
    @@ -145,6 +145,6 @@
     if __name__ == '__main__':
         seldom.main(browser="edge")
     
    - + diff --git a/web-testing/chaining.html b/web-testing/chaining.html index a312d1b..a690259 100644 --- a/web-testing/chaining.html +++ b/web-testing/chaining.html @@ -24,7 +24,7 @@ 链式调用 | seldom文档 - + - + diff --git a/web-testing/other.html b/web-testing/other.html index 46db272..fd8848d 100644 --- a/web-testing/other.html +++ b/web-testing/other.html @@ -24,7 +24,7 @@ 浏览器启动配置 | seldom文档 - +

    浏览器启动配置

    selenium 在启动浏览器的时候可以做很多配置,seldom 试图简化这些配置,但是总有很多情况兼顾不到。

    seldom 3.2 版本开放了这些配置,你只需要将配置传给 seldom 即可。

    使用headless模式

    Firefox和Chrome浏览器支持headless模式,将浏览器置于后台运行,这样不会影响到我们在测试机上完成其他工作。

    • chrome
    import seldom
    @@ -189,6 +189,6 @@
         seldom.main(browser=browser)
     
     
    - + diff --git a/web-testing/page_object.html b/web-testing/page_object.html index 5ee7299..0b49b49 100644 --- a/web-testing/page_object.html +++ b/web-testing/page_object.html @@ -24,7 +24,7 @@ Page Object | seldom文档 - + - + diff --git a/web-testing/seldom_api.html b/web-testing/seldom_api.html index 34334c5..04c1ce6 100644 --- a/web-testing/seldom_api.html +++ b/web-testing/seldom_api.html @@ -24,7 +24,7 @@ Seldom API | seldom文档 - + - +