mirror of
https://github.com/httprunner/httprunner.git
synced 2026-09-08 17:09:33 +08:00
Merge branch 'dev' into 'master'
release v5.0.0 See merge request iesqa/httprunner!107
This commit is contained in:
@@ -36,6 +36,7 @@ var (
|
|||||||
genHTMLReport bool
|
genHTMLReport bool
|
||||||
caseTimeout float32
|
caseTimeout float32
|
||||||
runMCPConfigPath string // MCP config path for run command
|
runMCPConfigPath string // MCP config path for run command
|
||||||
|
autoPopupHandler bool // enable auto popup handler for all steps
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -48,6 +49,7 @@ func init() {
|
|||||||
CmdRun.Flags().BoolVarP(&genHTMLReport, "gen-html-report", "g", false, "generate html report")
|
CmdRun.Flags().BoolVarP(&genHTMLReport, "gen-html-report", "g", false, "generate html report")
|
||||||
CmdRun.Flags().Float32Var(&caseTimeout, "case-timeout", 3600, "set testcase timeout (seconds)")
|
CmdRun.Flags().Float32Var(&caseTimeout, "case-timeout", 3600, "set testcase timeout (seconds)")
|
||||||
CmdRun.Flags().StringVar(&runMCPConfigPath, "mcp-config", "", "path to the MCP config file")
|
CmdRun.Flags().StringVar(&runMCPConfigPath, "mcp-config", "", "path to the MCP config file")
|
||||||
|
CmdRun.Flags().BoolVar(&autoPopupHandler, "enable-auto-popup-handler", false, "enable auto popup handler for all UI steps")
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeHRPRunner() *hrp.HRPRunner {
|
func makeHRPRunner() *hrp.HRPRunner {
|
||||||
@@ -76,5 +78,8 @@ func makeHRPRunner() *hrp.HRPRunner {
|
|||||||
if runMCPConfigPath != "" {
|
if runMCPConfigPath != "" {
|
||||||
runner.SetMCPConfigPath(runMCPConfigPath)
|
runner.SetMCPConfigPath(runMCPConfigPath)
|
||||||
}
|
}
|
||||||
|
if autoPopupHandler {
|
||||||
|
runner.EnableAutoPopupHandler(autoPopupHandler)
|
||||||
|
}
|
||||||
return runner
|
return runner
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -63,4 +63,4 @@ Copyright © 2017-present debugtalk. Apache-2.0 License.
|
|||||||
* [hrp startproject](hrp_startproject.md) - Create a scaffold project
|
* [hrp startproject](hrp_startproject.md) - Create a scaffold project
|
||||||
* [hrp wiki](hrp_wiki.md) - visit https://httprunner.com
|
* [hrp wiki](hrp_wiki.md) - visit https://httprunner.com
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 8-Jun-2025
|
###### Auto generated by spf13/cobra on 27-Jun-2025
|
||||||
|
|||||||
+1
-1
@@ -23,4 +23,4 @@ simple utils for android device management
|
|||||||
* [hrp adb install](hrp_adb_install.md) - push package to the device and install them automatically
|
* [hrp adb install](hrp_adb_install.md) - push package to the device and install them automatically
|
||||||
* [hrp adb screencap](hrp_adb_screencap.md) - Start android screen capture
|
* [hrp adb screencap](hrp_adb_screencap.md) - Start android screen capture
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 8-Jun-2025
|
###### Auto generated by spf13/cobra on 27-Jun-2025
|
||||||
|
|||||||
@@ -24,4 +24,4 @@ hrp adb devices [flags]
|
|||||||
|
|
||||||
* [hrp adb](hrp_adb.md) - simple utils for android device management
|
* [hrp adb](hrp_adb.md) - simple utils for android device management
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 8-Jun-2025
|
###### Auto generated by spf13/cobra on 27-Jun-2025
|
||||||
|
|||||||
@@ -28,4 +28,4 @@ hrp adb install [flags] PACKAGE
|
|||||||
|
|
||||||
* [hrp adb](hrp_adb.md) - simple utils for android device management
|
* [hrp adb](hrp_adb.md) - simple utils for android device management
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 8-Jun-2025
|
###### Auto generated by spf13/cobra on 27-Jun-2025
|
||||||
|
|||||||
@@ -25,4 +25,4 @@ hrp adb screencap [flags]
|
|||||||
|
|
||||||
* [hrp adb](hrp_adb.md) - simple utils for android device management
|
* [hrp adb](hrp_adb.md) - simple utils for android device management
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 8-Jun-2025
|
###### Auto generated by spf13/cobra on 27-Jun-2025
|
||||||
|
|||||||
@@ -36,4 +36,4 @@ hrp build $path ... [flags]
|
|||||||
|
|
||||||
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 8-Jun-2025
|
###### Auto generated by spf13/cobra on 27-Jun-2025
|
||||||
|
|||||||
@@ -34,4 +34,4 @@ hrp convert $path... [flags]
|
|||||||
|
|
||||||
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 8-Jun-2025
|
###### Auto generated by spf13/cobra on 27-Jun-2025
|
||||||
|
|||||||
+1
-1
@@ -29,4 +29,4 @@ simple utils for ios device management
|
|||||||
* [hrp ios uninstall](hrp_ios_uninstall.md) - uninstall package automatically
|
* [hrp ios uninstall](hrp_ios_uninstall.md) - uninstall package automatically
|
||||||
* [hrp ios xctest](hrp_ios_xctest.md) - run xctest
|
* [hrp ios xctest](hrp_ios_xctest.md) - run xctest
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 8-Jun-2025
|
###### Auto generated by spf13/cobra on 27-Jun-2025
|
||||||
|
|||||||
@@ -26,4 +26,4 @@ hrp ios apps [flags]
|
|||||||
|
|
||||||
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 8-Jun-2025
|
###### Auto generated by spf13/cobra on 27-Jun-2025
|
||||||
|
|||||||
@@ -24,4 +24,4 @@ hrp ios devices [flags]
|
|||||||
|
|
||||||
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 8-Jun-2025
|
###### Auto generated by spf13/cobra on 27-Jun-2025
|
||||||
|
|||||||
@@ -25,4 +25,4 @@ hrp ios install [flags] PACKAGE
|
|||||||
|
|
||||||
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 8-Jun-2025
|
###### Auto generated by spf13/cobra on 27-Jun-2025
|
||||||
|
|||||||
@@ -28,4 +28,4 @@ hrp ios mount [flags]
|
|||||||
|
|
||||||
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 8-Jun-2025
|
###### Auto generated by spf13/cobra on 27-Jun-2025
|
||||||
|
|||||||
@@ -26,4 +26,4 @@ hrp ios ps [flags]
|
|||||||
|
|
||||||
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 8-Jun-2025
|
###### Auto generated by spf13/cobra on 27-Jun-2025
|
||||||
|
|||||||
@@ -25,4 +25,4 @@ hrp ios reboot [flags]
|
|||||||
|
|
||||||
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 8-Jun-2025
|
###### Auto generated by spf13/cobra on 27-Jun-2025
|
||||||
|
|||||||
@@ -24,4 +24,4 @@ hrp ios tunnel [flags]
|
|||||||
|
|
||||||
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 8-Jun-2025
|
###### Auto generated by spf13/cobra on 27-Jun-2025
|
||||||
|
|||||||
@@ -26,4 +26,4 @@ hrp ios uninstall [flags] PACKAGE
|
|||||||
|
|
||||||
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 8-Jun-2025
|
###### Auto generated by spf13/cobra on 27-Jun-2025
|
||||||
|
|||||||
@@ -28,4 +28,4 @@ hrp ios xctest [flags]
|
|||||||
|
|
||||||
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
* [hrp ios](hrp_ios.md) - simple utils for ios device management
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 8-Jun-2025
|
###### Auto generated by spf13/cobra on 27-Jun-2025
|
||||||
|
|||||||
@@ -28,4 +28,4 @@ hrp mcp-server [flags]
|
|||||||
|
|
||||||
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 8-Jun-2025
|
###### Auto generated by spf13/cobra on 27-Jun-2025
|
||||||
|
|||||||
@@ -31,4 +31,4 @@ hrp mcphost [flags]
|
|||||||
|
|
||||||
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 8-Jun-2025
|
###### Auto generated by spf13/cobra on 27-Jun-2025
|
||||||
|
|||||||
@@ -24,4 +24,4 @@ hrp pytest $path ... [flags]
|
|||||||
|
|
||||||
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 8-Jun-2025
|
###### Auto generated by spf13/cobra on 27-Jun-2025
|
||||||
|
|||||||
@@ -33,4 +33,4 @@ hrp report [result_folder] [flags]
|
|||||||
|
|
||||||
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 8-Jun-2025
|
###### Auto generated by spf13/cobra on 27-Jun-2025
|
||||||
|
|||||||
+2
-1
@@ -23,6 +23,7 @@ hrp run $path... [flags]
|
|||||||
```
|
```
|
||||||
--case-timeout float32 set testcase timeout (seconds) (default 3600)
|
--case-timeout float32 set testcase timeout (seconds) (default 3600)
|
||||||
-c, --continue-on-failure continue running next step when failure occurs
|
-c, --continue-on-failure continue running next step when failure occurs
|
||||||
|
--enable-auto-popup-handler enable auto popup handler for all UI steps
|
||||||
-g, --gen-html-report generate html report
|
-g, --gen-html-report generate html report
|
||||||
-h, --help help for run
|
-h, --help help for run
|
||||||
--http-stat turn on HTTP latency stat (DNSLookup, TCP Connection, etc.)
|
--http-stat turn on HTTP latency stat (DNSLookup, TCP Connection, etc.)
|
||||||
@@ -45,4 +46,4 @@ hrp run $path... [flags]
|
|||||||
|
|
||||||
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 8-Jun-2025
|
###### Auto generated by spf13/cobra on 27-Jun-2025
|
||||||
|
|||||||
@@ -30,4 +30,4 @@ hrp server start [flags]
|
|||||||
|
|
||||||
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 8-Jun-2025
|
###### Auto generated by spf13/cobra on 27-Jun-2025
|
||||||
|
|||||||
@@ -29,4 +29,4 @@ hrp startproject $project_name [flags]
|
|||||||
|
|
||||||
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 8-Jun-2025
|
###### Auto generated by spf13/cobra on 27-Jun-2025
|
||||||
|
|||||||
@@ -24,4 +24,4 @@ hrp wiki [flags]
|
|||||||
|
|
||||||
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
* [hrp](hrp.md) - All-in-One Testing Framework for API, UI and Performance
|
||||||
|
|
||||||
###### Auto generated by spf13/cobra on 8-Jun-2025
|
###### Auto generated by spf13/cobra on 27-Jun-2025
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"name": "2048 小游戏自动化测试",
|
||||||
|
"ai_options": {
|
||||||
|
"llm_service": "doubao-1.5-ui-tars-250328"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"teststeps": [
|
||||||
|
{
|
||||||
|
"name": "启动抖音「2048经典」小游戏",
|
||||||
|
"android": {
|
||||||
|
"os_type": "android",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"method": "start_to_goal",
|
||||||
|
"params": "启动抖音,搜索「2048经典」小游戏,并启动游戏",
|
||||||
|
"options": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"validate": [
|
||||||
|
{
|
||||||
|
"check": "ui_ai",
|
||||||
|
"assert": "ai_assert",
|
||||||
|
"expect": "当前位于抖音「2048」小游戏页面",
|
||||||
|
"msg": "assert ai prompt [当前位于抖音「2048」小游戏页面] failed"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "开始游戏",
|
||||||
|
"android": {
|
||||||
|
"os_type": "android",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"method": "start_to_goal",
|
||||||
|
"params": "2048 是一款数字合并类的益智小游戏,以下是它的基本规则:\n1、游戏目标:在一个 4x4 的网格中,通过合并相同数字的方块,最终得到一个数值为 2048 的方块。当然,若你能继续合并,也可追求更高的数字。\n2、初始状态:游戏开始时,网格中会随机出现两个数字为 2 或 4 的方块。\n3、移动操作:玩家可以选择上、下、左、右四个方向进行移动。每次移动时,所有方块会朝着指定方向滑动,直到碰到边界或其他方块。\n4、合并规则:当两个相同数字的方块在移动过程中相遇时,它们会合并成一个新的方块,新方块的数值为原来两个方块数值之和。例如,两个 2 合并成一个 4,两个 4 合并成一个 8,依此类推。\n5、新方块生成:每次移动结束后,网格中会随机出现一个新的数字为 2 或 4 的方块。\n6、注意事项:若连续多次滑动无法生效,请调整策略;例如,向上无法滑动,可以尝试向下滑;向左无法滑动,可以尝试向右滑。\n7、游戏结束:当网格被填满,且没有可合并的方块时,游戏结束,停止游戏。\n\n请严格按照以上游戏规则,开始游戏\n",
|
||||||
|
"options": {
|
||||||
|
"max_retry_times": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package game_2048
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
hrp "github.com/httprunner/httprunner/v5"
|
||||||
|
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGame2048(t *testing.T) {
|
||||||
|
userInstruction := `2048 是一款数字合并类的益智小游戏,以下是它的基本规则:
|
||||||
|
1、游戏目标:在一个 4x4 的网格中,通过合并相同数字的方块,最终得到一个数值为 2048 的方块。当然,若你能继续合并,也可追求更高的数字。
|
||||||
|
2、初始状态:游戏开始时,网格中会随机出现两个数字为 2 或 4 的方块。
|
||||||
|
3、移动操作:玩家可以选择上、下、左、右四个方向进行移动。每次移动时,所有方块会朝着指定方向滑动,直到碰到边界或其他方块。
|
||||||
|
4、合并规则:当两个相同数字的方块在移动过程中相遇时,它们会合并成一个新的方块,新方块的数值为原来两个方块数值之和。例如,两个 2 合并成一个 4,两个 4 合并成一个 8,依此类推。
|
||||||
|
5、新方块生成:每次移动结束后,网格中会随机出现一个新的数字为 2 或 4 的方块。
|
||||||
|
6、注意事项:若连续多次滑动无法生效,请调整策略;例如,向上无法滑动,可以尝试向下滑;向左无法滑动,可以尝试向右滑。
|
||||||
|
7、游戏结束:当网格被填满,且没有可合并的方块时,游戏结束,停止游戏。
|
||||||
|
|
||||||
|
请严格按照以上游戏规则,开始游戏
|
||||||
|
`
|
||||||
|
|
||||||
|
testCase := &hrp.TestCase{
|
||||||
|
Config: hrp.NewConfig("2048 小游戏自动化测试").
|
||||||
|
SetLLMService(option.DOUBAO_1_5_UI_TARS_250328),
|
||||||
|
TestSteps: []hrp.IStep{
|
||||||
|
hrp.NewStep("启动抖音「2048经典」小游戏").
|
||||||
|
Android().
|
||||||
|
StartToGoal("启动抖音,搜索「2048经典」小游戏,并启动游戏").
|
||||||
|
Validate().
|
||||||
|
AssertAI("当前位于抖音「2048」小游戏页面"),
|
||||||
|
hrp.NewStep("开始游戏").
|
||||||
|
Android().
|
||||||
|
StartToGoal(userInstruction, option.WithMaxRetryTimes(100)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := testCase.Dump2JSON("game_2048.json")
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// err = hrp.NewRunner(t).Run(testCase)
|
||||||
|
// assert.Nil(t, err)
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"name": "连连看小游戏自动化测试",
|
||||||
|
"ai_options": {
|
||||||
|
"llm_service": "doubao-1.5-thinking-vision-pro-250428"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"teststeps": [
|
||||||
|
{
|
||||||
|
"name": "启动抖音「连了又连」小游戏",
|
||||||
|
"android": {
|
||||||
|
"os_type": "android",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"method": "start_to_goal",
|
||||||
|
"params": "启动抖音,搜索「连了又连」小游戏,并启动游戏",
|
||||||
|
"options": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"validate": [
|
||||||
|
{
|
||||||
|
"check": "ui_ai",
|
||||||
|
"assert": "ai_assert",
|
||||||
|
"expect": "当前位于抖音「连了又连」小游戏页面",
|
||||||
|
"msg": "assert ai prompt [当前位于抖音「连了又连」小游戏页面] failed"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "开始游戏",
|
||||||
|
"android": {
|
||||||
|
"os_type": "android",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"method": "start_to_goal",
|
||||||
|
"params": "连连看是一款经典的益智消除类小游戏,通常以图案或图标为主要元素。以下是连连看的基本规则说明:\n1. 游戏目标: 玩家需要通过连接相同的图案或图标,将它们从游戏界面中消除。\n2. 连接规则:\n- 两个相同的图案可以通过不超过三条直线连接。\n- 连接线可以水平或垂直,但不能斜线,也不能跨过其他图案。\n- 连接线的转折次数不能超过两次。\n3. 游戏界面:\n- 游戏界面是一个矩形区域,内含多个图案或图标,排列成行和列;图案或图标在未选中状态下背景为白色,选中状态下背景为绿色。\n- 游戏界面下方是道具区域,共有 3 种道具,从左到右分别是:「高亮显示」、「随机打乱」、「减少种类」。\n4、游戏攻略:建议多次使用道具,可以降低游戏难度\n- 优先使用「减少种类」道具,可以将图案种类随机减少一种\n- 遇到困难时,推荐使用「随机打乱」道具,可以获得很多新的消除机会\n- 观看广告视频,待屏幕右上角出现「领取成功」后,点击其右侧的 X 即可关闭广告,继续游戏\n\n请严格按照以上游戏规则,开始游戏\n",
|
||||||
|
"options": {
|
||||||
|
"max_retry_times": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
hrp "github.com/httprunner/httprunner/v5"
|
||||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||||
"github.com/httprunner/httprunner/v5/uixt/ai"
|
"github.com/httprunner/httprunner/v5/uixt/ai"
|
||||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||||
@@ -15,6 +16,45 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestGameLianliankan(t *testing.T) {
|
||||||
|
userInstruction := `连连看是一款经典的益智消除类小游戏,通常以图案或图标为主要元素。以下是连连看的基本规则说明:
|
||||||
|
1. 游戏目标: 玩家需要通过连接相同的图案或图标,将它们从游戏界面中消除。
|
||||||
|
2. 连接规则:
|
||||||
|
- 两个相同的图案可以通过不超过三条直线连接。
|
||||||
|
- 连接线可以水平或垂直,但不能斜线,也不能跨过其他图案。
|
||||||
|
- 连接线的转折次数不能超过两次。
|
||||||
|
3. 游戏界面:
|
||||||
|
- 游戏界面是一个矩形区域,内含多个图案或图标,排列成行和列;图案或图标在未选中状态下背景为白色,选中状态下背景为绿色。
|
||||||
|
- 游戏界面下方是道具区域,共有 3 种道具,从左到右分别是:「高亮显示」、「随机打乱」、「减少种类」。
|
||||||
|
4、游戏攻略:建议多次使用道具,可以降低游戏难度
|
||||||
|
- 优先使用「减少种类」道具,可以将图案种类随机减少一种
|
||||||
|
- 遇到困难时,推荐使用「随机打乱」道具,可以获得很多新的消除机会
|
||||||
|
- 观看广告视频,待屏幕右上角出现「领取成功」后,点击其右侧的 X 即可关闭广告,继续游戏
|
||||||
|
|
||||||
|
请严格按照以上游戏规则,开始游戏
|
||||||
|
`
|
||||||
|
|
||||||
|
testCase := &hrp.TestCase{
|
||||||
|
Config: hrp.NewConfig("连连看小游戏自动化测试").
|
||||||
|
SetLLMService(option.DOUBAO_1_5_THINKING_VISION_PRO_250428),
|
||||||
|
TestSteps: []hrp.IStep{
|
||||||
|
hrp.NewStep("启动抖音「连了又连」小游戏").
|
||||||
|
Android().
|
||||||
|
StartToGoal("启动抖音,搜索「连了又连」小游戏,并启动游戏").
|
||||||
|
Validate().
|
||||||
|
AssertAI("当前位于抖音「连了又连」小游戏页面"),
|
||||||
|
hrp.NewStep("开始游戏").
|
||||||
|
Android().
|
||||||
|
StartToGoal(userInstruction, option.WithMaxRetryTimes(100)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := testCase.Dump2JSON("game_llk.json")
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// err = hrp.NewRunner(t).Run(testCase)
|
||||||
|
// assert.Nil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
// convertToGameElementFromQueryResult converts AI query result to GameElement for testing
|
// convertToGameElementFromQueryResult converts AI query result to GameElement for testing
|
||||||
func convertToGameElementFromQueryResult(result *ai.QueryResult) (*GameElement, error) {
|
func convertToGameElementFromQueryResult(result *ai.QueryResult) (*GameElement, error) {
|
||||||
if result == nil {
|
if result == nil {
|
||||||
|
|||||||
@@ -6,16 +6,9 @@
|
|||||||
},
|
},
|
||||||
"ios": [
|
"ios": [
|
||||||
{
|
{
|
||||||
"perf_options": {
|
|
||||||
"sys_cpu": true,
|
|
||||||
"sys_mem": true,
|
|
||||||
"fps": true,
|
|
||||||
"network": true
|
|
||||||
},
|
|
||||||
"port": 8700,
|
"port": 8700,
|
||||||
"mjpeg_port": 8800,
|
"mjpeg_port": 8800,
|
||||||
"log_on": true,
|
"log_on": true
|
||||||
"xctest_bundle_id": "com.gtf.wda.runner.xctrunner"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -23,6 +16,7 @@
|
|||||||
{
|
{
|
||||||
"name": "启动抖音",
|
"name": "启动抖音",
|
||||||
"ios": {
|
"ios": {
|
||||||
|
"os_type": "ios",
|
||||||
"actions": [
|
"actions": [
|
||||||
{
|
{
|
||||||
"method": "home"
|
"method": "home"
|
||||||
@@ -34,8 +28,10 @@
|
|||||||
{
|
{
|
||||||
"method": "swipe_to_tap_app",
|
"method": "swipe_to_tap_app",
|
||||||
"params": "$app_name",
|
"params": "$app_name",
|
||||||
|
"options": {
|
||||||
"identifier": "启动抖音",
|
"identifier": "启动抖音",
|
||||||
"max_retry_times": 5
|
"max_retry_times": 5
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"method": "sleep",
|
"method": "sleep",
|
||||||
@@ -55,23 +51,29 @@
|
|||||||
{
|
{
|
||||||
"name": "处理青少年弹窗",
|
"name": "处理青少年弹窗",
|
||||||
"ios": {
|
"ios": {
|
||||||
|
"os_type": "ios",
|
||||||
"actions": [
|
"actions": [
|
||||||
{
|
{
|
||||||
"method": "tap_ocr",
|
"method": "tap_ocr",
|
||||||
"params": "我知道了",
|
"params": "我知道了",
|
||||||
|
"options": {
|
||||||
"ignore_NotFoundError": true
|
"ignore_NotFoundError": true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "进入购物页",
|
"name": "进入购物页",
|
||||||
"ios": {
|
"ios": {
|
||||||
|
"os_type": "ios",
|
||||||
"actions": [
|
"actions": [
|
||||||
{
|
{
|
||||||
"method": "tap_ocr",
|
"method": "tap_ocr",
|
||||||
"params": "商城",
|
"params": "商城",
|
||||||
|
"options": {
|
||||||
"identifier": "点击商城"
|
"identifier": "点击商城"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"method": "sleep",
|
"method": "sleep",
|
||||||
@@ -83,11 +85,14 @@
|
|||||||
{
|
{
|
||||||
"name": "进入推荐页",
|
"name": "进入推荐页",
|
||||||
"ios": {
|
"ios": {
|
||||||
|
"os_type": "ios",
|
||||||
"actions": [
|
"actions": [
|
||||||
{
|
{
|
||||||
"method": "tap_ocr",
|
"method": "tap_ocr",
|
||||||
"params": "推荐",
|
"params": "推荐",
|
||||||
|
"options": {
|
||||||
"identifier": "点击推荐"
|
"identifier": "点击推荐"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"method": "sleep",
|
"method": "sleep",
|
||||||
@@ -99,29 +104,36 @@
|
|||||||
{
|
{
|
||||||
"name": "向上滑动 2 次",
|
"name": "向上滑动 2 次",
|
||||||
"ios": {
|
"ios": {
|
||||||
|
"os_type": "ios",
|
||||||
"actions": [
|
"actions": [
|
||||||
{
|
{
|
||||||
"method": "swipe",
|
"method": "swipe_direction",
|
||||||
"params": "up",
|
"params": "up",
|
||||||
|
"options": {
|
||||||
"identifier": "第 1 次上划"
|
"identifier": "第 1 次上划"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"method": "sleep",
|
"method": "sleep",
|
||||||
"params": 2
|
"params": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"method": "swipe",
|
"method": "swipe_direction",
|
||||||
"params": "up",
|
"params": "up",
|
||||||
|
"options": {
|
||||||
"identifier": "第 2 次上划"
|
"identifier": "第 2 次上划"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"method": "sleep",
|
"method": "sleep",
|
||||||
"params": 2
|
"params": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"method": "swipe",
|
"method": "swipe_direction",
|
||||||
"params": "up",
|
"params": "up",
|
||||||
|
"options": {
|
||||||
"identifier": "第 3 次上划"
|
"identifier": "第 3 次上划"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"method": "sleep",
|
"method": "sleep",
|
||||||
@@ -133,7 +145,9 @@
|
|||||||
0.9,
|
0.9,
|
||||||
0.1
|
0.1
|
||||||
],
|
],
|
||||||
|
"options": {
|
||||||
"identifier": "点击进入搜索框"
|
"identifier": "点击进入搜索框"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"method": "sleep",
|
"method": "sleep",
|
||||||
@@ -141,9 +155,18 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"method": "input",
|
"method": "input",
|
||||||
"params": "httprunner",
|
"params": "httprunner 发版记录",
|
||||||
|
"options": {
|
||||||
"identifier": "输入搜索关键词"
|
"identifier": "输入搜索关键词"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"method": "tap_ocr",
|
||||||
|
"params": "搜索",
|
||||||
|
"options": {
|
||||||
|
"identifier": "点击搜索"
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ func TestWDALog(t *testing.T) {
|
|||||||
SwipeUp(option.WithIdentifier("第 2 次上划")).Sleep(2).
|
SwipeUp(option.WithIdentifier("第 2 次上划")).Sleep(2).
|
||||||
SwipeUp(option.WithIdentifier("第 3 次上划")).Sleep(2).
|
SwipeUp(option.WithIdentifier("第 3 次上划")).Sleep(2).
|
||||||
TapXY(0.9, 0.1, option.WithIdentifier("点击进入搜索框")).Sleep(2).
|
TapXY(0.9, 0.1, option.WithIdentifier("点击进入搜索框")).Sleep(2).
|
||||||
Input("httprunner", option.WithIdentifier("输入搜索关键词")),
|
Input("httprunner 发版记录", option.WithIdentifier("输入搜索关键词")).
|
||||||
|
TapByOCR("搜索", option.WithIdentifier("点击搜索")),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
v5.0.0-beta-2506261443
|
v5.0.0-250627
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/httprunner/funplugin"
|
||||||
"github.com/jinzhu/copier"
|
"github.com/jinzhu/copier"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
|
|
||||||
"github.com/httprunner/funplugin"
|
|
||||||
"github.com/httprunner/httprunner/v5/code"
|
"github.com/httprunner/httprunner/v5/code"
|
||||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||||
"github.com/httprunner/httprunner/v5/internal/config"
|
"github.com/httprunner/httprunner/v5/internal/config"
|
||||||
@@ -54,6 +54,7 @@ func NewRunner(t *testing.T) *HRPRunner {
|
|||||||
failfast: true, // default to failfast
|
failfast: true, // default to failfast
|
||||||
genHTMLReport: false,
|
genHTMLReport: false,
|
||||||
mcpConfigPath: "",
|
mcpConfigPath: "",
|
||||||
|
autoPopupHandler: false,
|
||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
@@ -87,6 +88,7 @@ type HRPRunner struct {
|
|||||||
saveTests bool
|
saveTests bool
|
||||||
genHTMLReport bool
|
genHTMLReport bool
|
||||||
mcpConfigPath string // MCP config file path
|
mcpConfigPath string // MCP config file path
|
||||||
|
autoPopupHandler bool // enable auto popup handler for all UI steps
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
http2Client *http.Client
|
http2Client *http.Client
|
||||||
wsDialer *websocket.Dialer
|
wsDialer *websocket.Dialer
|
||||||
@@ -191,8 +193,17 @@ func (r *HRPRunner) SetSaveTests(saveTests bool) *HRPRunner {
|
|||||||
|
|
||||||
// GenHTMLReport configures whether to gen html report of api tests.
|
// GenHTMLReport configures whether to gen html report of api tests.
|
||||||
func (r *HRPRunner) GenHTMLReport() *HRPRunner {
|
func (r *HRPRunner) GenHTMLReport() *HRPRunner {
|
||||||
log.Info().Bool("genHTMLReport", true).Msg("[init] SetgenHTMLReport")
|
log.Info().Bool("genHTMLReport", true).Bool("saveTests", true).
|
||||||
|
Msg("[init] SetGenHTMLReport")
|
||||||
r.genHTMLReport = true
|
r.genHTMLReport = true
|
||||||
|
r.saveTests = true
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableAutoPopupHandler configures whether to enable auto popup handler for all UI steps.
|
||||||
|
func (r *HRPRunner) EnableAutoPopupHandler(enabled bool) *HRPRunner {
|
||||||
|
log.Info().Bool("autoPopupHandler", enabled).Msg("[init] EnableAutoPopupHandler")
|
||||||
|
r.autoPopupHandler = enabled
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,6 +403,13 @@ func NewCaseRunner(testcase TestCase, hrpRunner *HRPRunner) (*CaseRunner, error)
|
|||||||
return nil, errors.Wrap(err, "parse testcase config failed")
|
return nil, errors.Wrap(err, "parse testcase config failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// apply global auto popup handler setting if enabled
|
||||||
|
// priority: command line > testcase config > step config
|
||||||
|
if hrpRunner.autoPopupHandler {
|
||||||
|
parsedConfig.AutoPopupHandler = true
|
||||||
|
log.Info().Bool("autoPopupHandler", true).Msg("applied global auto popup handler setting")
|
||||||
|
}
|
||||||
|
|
||||||
// set request timeout in seconds
|
// set request timeout in seconds
|
||||||
if parsedConfig.RequestTimeout != 0 {
|
if parsedConfig.RequestTimeout != 0 {
|
||||||
hrpRunner.SetRequestTimeout(parsedConfig.RequestTimeout)
|
hrpRunner.SetRequestTimeout(parsedConfig.RequestTimeout)
|
||||||
@@ -855,8 +873,6 @@ func (r *SessionRunner) RunStep(step IStep) (stepResult *StepResult, err error)
|
|||||||
stepName := step.Name()
|
stepName := step.Name()
|
||||||
stepType := string(step.Type())
|
stepType := string(step.Type())
|
||||||
|
|
||||||
log.Info().Str("step", stepName).Str("type", stepType).Msg(RUN_STEP_START)
|
|
||||||
|
|
||||||
// execute step with parameters iterator
|
// execute step with parameters iterator
|
||||||
tasks, err := r.generateExecutionTasks(step)
|
tasks, err := r.generateExecutionTasks(step)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -897,6 +913,8 @@ func (r *SessionRunner) RunStep(step IStep) (stepResult *StepResult, err error)
|
|||||||
|
|
||||||
// execute with loops as outer iteration
|
// execute with loops as outer iteration
|
||||||
for _, task := range tasks {
|
for _, task := range tasks {
|
||||||
|
log.Info().Str("step", task.stepName).Str("type", stepType).Msg(RUN_STEP_START)
|
||||||
|
|
||||||
// Check for interrupt signal before each parameter iteration
|
// Check for interrupt signal before each parameter iteration
|
||||||
select {
|
select {
|
||||||
case <-r.caseRunner.hrpRunner.interruptSignal:
|
case <-r.caseRunner.hrpRunner.interruptSignal:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ PRE_COMMIT_FILE=.git/hooks/pre-commit
|
|||||||
# install pre-commit hook and make it executable
|
# install pre-commit hook and make it executable
|
||||||
function install() {
|
function install() {
|
||||||
go get mvdan.cc/gofumpt
|
go get mvdan.cc/gofumpt
|
||||||
go get github.com/incu6us/goimports-reviser/v2@latest
|
go get github.com/incu6us/goimports-reviser/v3@latest
|
||||||
cat > $PRE_COMMIT_FILE <<'EOF'
|
cat > $PRE_COMMIT_FILE <<'EOF'
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ done
|
|||||||
version_file=internal/version/VERSION
|
version_file=internal/version/VERSION
|
||||||
|
|
||||||
# get current date
|
# get current date
|
||||||
current_date=$(date +"%y%m%d%H%M")
|
current_date=$(date +"%y%m%d")
|
||||||
|
|
||||||
# update version
|
# update version
|
||||||
sed -i '' "s/[0-9]\{10\}/${current_date}/" "$version_file"
|
sed -i '' "s/[0-9]\{10\}/${current_date}/" "$version_file"
|
||||||
|
|||||||
@@ -765,6 +765,10 @@ func runStepMobileUI(s *SessionRunner, step IStep) (stepResult *StepResult, err
|
|||||||
identifier = action.Identifier
|
identifier = action.Identifier
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
if action.Options != nil && action.Options.Identifier != "" {
|
||||||
|
identifier = action.Options.Identifier
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stepResult.Identifier = identifier
|
stepResult.Identifier = identifier
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
hrp "github.com/httprunner/httprunner/v5"
|
hrp "github.com/httprunner/httprunner/v5"
|
||||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GameInfo 定义游戏界面分析的输出格式
|
// GameInfo 定义游戏界面分析的输出格式
|
||||||
@@ -111,79 +110,6 @@ func TestAndroidAction(t *testing.T) {
|
|||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGameLianliankan(t *testing.T) {
|
|
||||||
userInstruction := `连连看是一款经典的益智消除类小游戏,通常以图案或图标为主要元素。以下是连连看的基本规则说明:
|
|
||||||
1. 游戏目标: 玩家需要通过连接相同的图案或图标,将它们从游戏界面中消除。
|
|
||||||
2. 连接规则:
|
|
||||||
- 两个相同的图案可以通过不超过三条直线连接。
|
|
||||||
- 连接线可以水平或垂直,但不能斜线,也不能跨过其他图案。
|
|
||||||
- 连接线的转折次数不能超过两次。
|
|
||||||
3. 游戏界面:
|
|
||||||
- 游戏界面是一个矩形区域,内含多个图案或图标,排列成行和列;图案或图标在未选中状态下背景为白色,选中状态下背景为绿色。
|
|
||||||
- 游戏界面下方是道具区域,共有 3 种道具,从左到右分别是:「高亮显示」、「随机打乱」、「减少种类」。
|
|
||||||
4、游戏攻略:建议多次使用道具,可以降低游戏难度
|
|
||||||
- 优先使用「减少种类」道具,可以将图案种类随机减少一种
|
|
||||||
- 遇到困难时,推荐使用「随机打乱」道具,可以获得很多新的消除机会
|
|
||||||
- 观看广告视频,待屏幕右上角出现「领取成功」后,点击其右侧的 X 即可关闭广告,继续游戏
|
|
||||||
|
|
||||||
请严格按照以上游戏规则,开始游戏
|
|
||||||
`
|
|
||||||
|
|
||||||
testCase := &hrp.TestCase{
|
|
||||||
Config: hrp.NewConfig("连连看小游戏自动化测试").
|
|
||||||
SetLLMService(option.DOUBAO_1_5_THINKING_VISION_PRO_250428),
|
|
||||||
TestSteps: []hrp.IStep{
|
|
||||||
hrp.NewStep("启动抖音「连了又连」小游戏").
|
|
||||||
Android().
|
|
||||||
StartToGoal("启动抖音,搜索「连了又连」小游戏,并启动游戏").
|
|
||||||
Validate().
|
|
||||||
AssertAI("当前位于抖音「连了又连」小游戏页面"),
|
|
||||||
hrp.NewStep("开始游戏").
|
|
||||||
Android().
|
|
||||||
StartToGoal(userInstruction, option.WithMaxRetryTimes(100)),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err := testCase.Dump2JSON("game_llk.json")
|
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
err = hrp.NewRunner(t).Run(testCase)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGame2048(t *testing.T) {
|
|
||||||
userInstruction := `2048 是一款数字合并类的益智小游戏,以下是它的基本规则:
|
|
||||||
1、游戏目标:在一个 4x4 的网格中,通过合并相同数字的方块,最终得到一个数值为 2048 的方块。当然,若你能继续合并,也可追求更高的数字。
|
|
||||||
2、初始状态:游戏开始时,网格中会随机出现两个数字为 2 或 4 的方块。
|
|
||||||
3、移动操作:玩家可以选择上、下、左、右四个方向进行移动。每次移动时,所有方块会朝着指定方向滑动,直到碰到边界或其他方块。
|
|
||||||
4、合并规则:当两个相同数字的方块在移动过程中相遇时,它们会合并成一个新的方块,新方块的数值为原来两个方块数值之和。例如,两个 2 合并成一个 4,两个 4 合并成一个 8,依此类推。
|
|
||||||
5、新方块生成:每次移动结束后,网格中会随机出现一个新的数字为 2 或 4 的方块。
|
|
||||||
6、注意事项:若连续多次滑动无法生效,请调整策略;例如,向上无法滑动,可以尝试向下滑;向左无法滑动,可以尝试向右滑。
|
|
||||||
7、游戏结束:当网格被填满,且没有可合并的方块时,游戏结束,停止游戏。
|
|
||||||
|
|
||||||
请严格按照以上游戏规则,开始游戏
|
|
||||||
`
|
|
||||||
|
|
||||||
testCase := &hrp.TestCase{
|
|
||||||
Config: hrp.NewConfig("2048 小游戏自动化测试").
|
|
||||||
SetLLMService(option.DOUBAO_1_5_UI_TARS_250328),
|
|
||||||
TestSteps: []hrp.IStep{
|
|
||||||
hrp.NewStep("启动抖音「2048经典」小游戏").
|
|
||||||
Android().
|
|
||||||
StartToGoal("启动抖音,搜索「2048经典」小游戏,并启动游戏").
|
|
||||||
Validate().
|
|
||||||
AssertAI("当前位于抖音「2048」小游戏页面"),
|
|
||||||
hrp.NewStep("开始游戏").
|
|
||||||
Android().
|
|
||||||
StartToGoal(userInstruction, option.WithMaxRetryTimes(100)),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err := testCase.Dump2JSON("game_2048.json")
|
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
// err = hrp.NewRunner(t).Run(testCase)
|
|
||||||
// assert.Nil(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAIAction(t *testing.T) {
|
func TestAIAction(t *testing.T) {
|
||||||
testCase := &hrp.TestCase{
|
testCase := &hrp.TestCase{
|
||||||
Config: hrp.NewConfig("run ui action with ai").
|
Config: hrp.NewConfig("run ui action with ai").
|
||||||
|
|||||||
+10
-2
@@ -135,9 +135,17 @@ func (p *Planner) Plan(ctx context.Context, opts *PlanningOptions) (result *Plan
|
|||||||
for _, toolCall := range message.ToolCalls {
|
for _, toolCall := range message.ToolCalls {
|
||||||
toolCallID += toolCall.ID
|
toolCallID += toolCall.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure content is not empty for Tool messages to avoid API 400 errors
|
||||||
|
// Some models may return empty content when using function calling
|
||||||
|
toolContent := message.Content
|
||||||
|
if toolContent == "" {
|
||||||
|
toolContent = "Function call initiated"
|
||||||
|
}
|
||||||
|
|
||||||
p.history.Append(&schema.Message{
|
p.history.Append(&schema.Message{
|
||||||
Role: schema.Tool,
|
Role: schema.Tool,
|
||||||
Content: message.Content,
|
Content: toolContent,
|
||||||
ToolCalls: message.ToolCalls,
|
ToolCalls: message.ToolCalls,
|
||||||
ToolCallID: toolCallID,
|
ToolCallID: toolCallID,
|
||||||
})
|
})
|
||||||
@@ -158,7 +166,7 @@ func (p *Planner) Plan(ctx context.Context, opts *PlanningOptions) (result *Plan
|
|||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
ModelName: string(p.modelConfig.ModelType),
|
ModelName: string(p.modelConfig.ModelType),
|
||||||
}
|
}
|
||||||
log.Debug().Str("reason", err.Error()).Msg("parse content to actions failed")
|
log.Warn().Str("reason", err.Error()).Msg("parse content to actions failed")
|
||||||
}
|
}
|
||||||
// append assistant message (since we're parsing content, not using native function calling)
|
// append assistant message (since we're parsing content, not using native function calling)
|
||||||
p.history.Append(&schema.Message{
|
p.history.Append(&schema.Message{
|
||||||
|
|||||||
@@ -731,11 +731,21 @@ func (ad *ADBDriver) ForegroundInfo() (app types.AppInfo, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return app, err
|
return app, err
|
||||||
}
|
}
|
||||||
err = json.Unmarshal([]byte(strings.TrimSpace(packageInfo)), &app)
|
|
||||||
|
// Clean packageInfo: remove null bytes that cause JSON parsing issues
|
||||||
|
packageInfo = strings.ReplaceAll(packageInfo, "\x00", "")
|
||||||
|
|
||||||
|
// Check for empty response after cleaning
|
||||||
|
if strings.TrimSpace(packageInfo) == "" {
|
||||||
|
return app, errors.New("empty response from evalite process")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal([]byte(packageInfo), &app)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Str("packageInfo", packageInfo).Msg("get foreground app failed")
|
log.Error().Err(err).Str("packageInfo", packageInfo).Msg("get foreground app failed")
|
||||||
|
return app, err
|
||||||
}
|
}
|
||||||
return
|
return app, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ad *ADBDriver) SetIme(imeRegx string) error {
|
func (ad *ADBDriver) SetIme(imeRegx string) error {
|
||||||
|
|||||||
@@ -424,6 +424,9 @@ func resizeImage(src image.Image, width, height int) image.Image {
|
|||||||
|
|
||||||
// CompressImageFile compresses an image file and returns the compressed data
|
// CompressImageFile compresses an image file and returns the compressed data
|
||||||
func CompressImageFile(imagePath string, enableResize bool, maxWidth int) ([]byte, error) {
|
func CompressImageFile(imagePath string, enableResize bool, maxWidth int) ([]byte, error) {
|
||||||
|
log.Debug().Str("imagePath", imagePath).Bool("enableResize", enableResize).
|
||||||
|
Int("maxWidth", maxWidth).Msg("compress image file")
|
||||||
|
|
||||||
// Read the original image file
|
// Read the original image file
|
||||||
file, err := os.Open(imagePath)
|
file, err := os.Open(imagePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+27
-9
@@ -87,6 +87,7 @@ func (s *DriverSession) GetData(withReset bool) SessionData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *DriverSession) SetBaseURL(baseUrl string) {
|
func (s *DriverSession) SetBaseURL(baseUrl string) {
|
||||||
|
log.Info().Str("baseUrl", baseUrl).Msg("set driver session base URL")
|
||||||
s.baseUrl = baseUrl
|
s.baseUrl = baseUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,25 +158,42 @@ func (s *DriverSession) DELETE(urlStr string) (rawResp DriverRawResponse, err er
|
|||||||
|
|
||||||
func (s *DriverSession) RequestWithRetry(method string, urlStr string, rawBody []byte) (
|
func (s *DriverSession) RequestWithRetry(method string, urlStr string, rawBody []byte) (
|
||||||
rawResp DriverRawResponse, err error) {
|
rawResp DriverRawResponse, err error) {
|
||||||
for count := 1; count <= s.maxRetry; count++ {
|
var lastError error
|
||||||
|
|
||||||
|
for attempt := 1; attempt <= s.maxRetry; attempt++ {
|
||||||
|
// Execute the request
|
||||||
rawResp, err = s.Request(method, urlStr, rawBody)
|
rawResp, err = s.Request(method, urlStr, rawBody)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
if attempt > 1 {
|
||||||
|
log.Info().Msgf("request succeeded after %d attempts", attempt)
|
||||||
}
|
}
|
||||||
|
return rawResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lastError = err
|
||||||
|
log.Warn().Err(err).Msgf("request failed, attempt %d/%d", attempt, s.maxRetry)
|
||||||
|
|
||||||
|
// If this was the last attempt, break
|
||||||
|
if attempt == s.maxRetry {
|
||||||
|
log.Error().Err(lastError).Msgf("all %d retry attempts failed, giving up", s.maxRetry)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait before next attempt
|
||||||
time.Sleep(3 * time.Second)
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
|
// Try to reset the session for the next attempt
|
||||||
if s.resetFn != nil {
|
if s.resetFn != nil {
|
||||||
log.Warn().Msg("reset driver session")
|
log.Warn().Msgf("attempting to reset driver session before attempt %d", attempt+1)
|
||||||
if err2 := s.resetFn(); err2 != nil {
|
if resetErr := s.resetFn(); resetErr != nil {
|
||||||
log.Error().Err(err2).Msgf(
|
log.Error().Err(resetErr).Msgf("failed to reset session, will retry without reset")
|
||||||
"failed to reset session, try count %v", count)
|
|
||||||
} else {
|
} else {
|
||||||
log.Info().Msgf(
|
log.Info().Msg("session reset successful")
|
||||||
"reset session success, try count %v", count)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
|
||||||
|
return nil, lastError
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DriverSession) Request(method string, urlStr string, rawBody []byte) (
|
func (s *DriverSession) Request(method string, urlStr string, rawBody []byte) (
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
package uixt
|
package uixt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||||
@@ -25,7 +24,7 @@ func TestWindowSize(t *testing.T) {
|
|||||||
driver := setupHDCDriverExt(t)
|
driver := setupHDCDriverExt(t)
|
||||||
size, err := driver.WindowSize()
|
size, err := driver.WindowSize()
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
t.Log(fmt.Sprintf("width: %d, height: %d", size.Width, size.Height))
|
assert.NotNil(t, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHarmonyTap(t *testing.T) {
|
func TestHarmonyTap(t *testing.T) {
|
||||||
|
|||||||
+3
-8
@@ -164,6 +164,9 @@ func (dev *IOSDevice) Setup() error {
|
|||||||
dev.DeviceEntry.UserspaceTUNPort = info.UserspaceTUNPort
|
dev.DeviceEntry.UserspaceTUNPort = info.UserspaceTUNPort
|
||||||
dev.DeviceEntry.UserspaceTUN = info.UserspaceTUN
|
dev.DeviceEntry.UserspaceTUN = info.UserspaceTUN
|
||||||
rsdService, err := ios.NewWithAddrPortDevice(info.Address, info.RsdPort, dev.DeviceEntry)
|
rsdService, err := ios.NewWithAddrPortDevice(info.Address, info.RsdPort, dev.DeviceEntry)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
defer rsdService.Close()
|
defer rsdService.Close()
|
||||||
rsdProvider, err := rsdService.Handshake()
|
rsdProvider, err := rsdService.Handshake()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -218,14 +221,6 @@ func (dev *IOSDevice) NewDriver() (driver IDriver, err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to init WDA driver")
|
return nil, errors.Wrap(err, "failed to init WDA driver")
|
||||||
}
|
}
|
||||||
settings, err := wdaDriver.SetAppiumSettings(map[string]interface{}{
|
|
||||||
"snapshotMaxDepth": dev.Options.SnapshotMaxDepth,
|
|
||||||
"acceptAlertButtonSelector": dev.Options.AcceptAlertButtonSelector,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to set appium WDA settings")
|
|
||||||
}
|
|
||||||
log.Info().Interface("appiumWDASettings", settings).Msg("set appium WDA settings")
|
|
||||||
|
|
||||||
if dev.Options.ResetHomeOnStartup {
|
if dev.Options.ResetHomeOnStartup {
|
||||||
log.Info().Msg("go back to home screen")
|
log.Info().Msg("go back to home screen")
|
||||||
|
|||||||
@@ -36,12 +36,18 @@ func NewWDADriver(device *IOSDevice) (*WDADriver, error) {
|
|||||||
Session: NewDriverSession(),
|
Session: NewDriverSession(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if !device.Options.LazySetup {
|
|
||||||
// setup driver
|
// setup driver
|
||||||
if err := driver.Setup(); err != nil {
|
if err := driver.Setup(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check WDA status
|
||||||
|
wdaStatus, err := driver.Status()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
log.Info().Interface("status", wdaStatus).
|
||||||
|
Msg("check WDA status")
|
||||||
|
|
||||||
// register driver session reset handler
|
// register driver session reset handler
|
||||||
driver.Session.RegisterResetHandler(driver.Setup)
|
driver.Session.RegisterResetHandler(driver.Setup)
|
||||||
@@ -665,7 +671,7 @@ func (wd *WDADriver) Input(text string, opts ...option.ActionOption) (err error)
|
|||||||
// [[FBRoute POST:@"/wda/keys"] respondWithTarget:self action:@selector(handleKeys:)]
|
// [[FBRoute POST:@"/wda/keys"] respondWithTarget:self action:@selector(handleKeys:)]
|
||||||
data := map[string]interface{}{"value": strings.Split(text, "")}
|
data := map[string]interface{}{"value": strings.Split(text, "")}
|
||||||
option.MergeOptions(data, opts...)
|
option.MergeOptions(data, opts...)
|
||||||
_, err = wd.Session.POST(data, "/gtf/interaction/input")
|
_, err = wd.Session.POST(data, "/wings/interaction/keys")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -38,8 +38,7 @@ func TestDevice_IOS_Install(t *testing.T) {
|
|||||||
func TestDriver_WDA_LazySetup(t *testing.T) {
|
func TestDriver_WDA_LazySetup(t *testing.T) {
|
||||||
device, err := NewIOSDevice(
|
device, err := NewIOSDevice(
|
||||||
option.WithWDAPort(8700),
|
option.WithWDAPort(8700),
|
||||||
option.WithWDAMjpegPort(8800),
|
option.WithWDAMjpegPort(8800))
|
||||||
option.WithLazySetup(true))
|
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
driver, err := NewWDADriver(device)
|
driver, err := NewWDADriver(device)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|||||||
+25
-5
@@ -193,6 +193,8 @@ func extractActionOptionsToArguments(actionOptions []option.ActionOption, argume
|
|||||||
"tap_random_rect": tempOptions.TapRandomRect,
|
"tap_random_rect": tempOptions.TapRandomRect,
|
||||||
"anti_risk": tempOptions.AntiRisk,
|
"anti_risk": tempOptions.AntiRisk,
|
||||||
"pre_mark_operation": tempOptions.PreMarkOperation,
|
"pre_mark_operation": tempOptions.PreMarkOperation,
|
||||||
|
"reset_history": tempOptions.ResetHistory,
|
||||||
|
"match_one": tempOptions.MatchOne,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add boolean options only if they are true
|
// Add boolean options only if they are true
|
||||||
@@ -209,6 +211,18 @@ func extractActionOptionsToArguments(actionOptions []option.ActionOption, argume
|
|||||||
if tempOptions.Index != 0 {
|
if tempOptions.Index != 0 {
|
||||||
arguments["index"] = tempOptions.Index
|
arguments["index"] = tempOptions.Index
|
||||||
}
|
}
|
||||||
|
if tempOptions.Interval > 0 {
|
||||||
|
arguments["interval"] = tempOptions.Interval
|
||||||
|
}
|
||||||
|
if tempOptions.Steps > 0 {
|
||||||
|
arguments["steps"] = tempOptions.Steps
|
||||||
|
}
|
||||||
|
if tempOptions.Timeout > 0 {
|
||||||
|
arguments["timeout"] = tempOptions.Timeout
|
||||||
|
}
|
||||||
|
if tempOptions.Frequency > 0 {
|
||||||
|
arguments["frequency"] = tempOptions.Frequency
|
||||||
|
}
|
||||||
// Only set duration if it's not already set (to avoid overriding tool-specific conversions)
|
// Only set duration if it's not already set (to avoid overriding tool-specific conversions)
|
||||||
if tempOptions.Duration > 0 {
|
if tempOptions.Duration > 0 {
|
||||||
if _, exists := arguments["duration"]; !exists {
|
if _, exists := arguments["duration"]; !exists {
|
||||||
@@ -288,13 +302,19 @@ func extractActionOptionsToArguments(actionOptions []option.ActionOption, argume
|
|||||||
if tempOptions.Selector != "" {
|
if tempOptions.Selector != "" {
|
||||||
arguments["selector"] = tempOptions.Selector
|
arguments["selector"] = tempOptions.Selector
|
||||||
}
|
}
|
||||||
}
|
if tempOptions.Identifier != "" {
|
||||||
|
arguments["identifier"] = tempOptions.Identifier
|
||||||
|
}
|
||||||
|
|
||||||
func getFloat64ValueOrDefault(value float64, defaultValue float64) float64 {
|
// Add direction option (can be string or []float64)
|
||||||
if value == 0 {
|
if tempOptions.Direction != nil {
|
||||||
return defaultValue
|
arguments["direction"] = tempOptions.Direction
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add custom options
|
||||||
|
if len(tempOptions.Custom) > 0 {
|
||||||
|
arguments["custom"] = tempOptions.Custom
|
||||||
}
|
}
|
||||||
return value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseActionOptions converts MCP request arguments to ActionOptions struct
|
// parseActionOptions converts MCP request arguments to ActionOptions struct
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
"github.com/mark3labs/mcp-go/server"
|
"github.com/mark3labs/mcp-go/server"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ToolStartToGoal implements the start_to_goal tool call.
|
// ToolStartToGoal implements the start_to_goal tool call.
|
||||||
@@ -162,7 +163,7 @@ func (t *ToolAIQuery) Implement() server.ToolHandlerFunc {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build action options from unified request
|
// Build all options from request arguments
|
||||||
opts := unifiedReq.Options()
|
opts := unifiedReq.Options()
|
||||||
|
|
||||||
// AI query logic with options
|
// AI query logic with options
|
||||||
|
|||||||
+20
-64
@@ -5,11 +5,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
|
||||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
"github.com/mark3labs/mcp-go/server"
|
"github.com/mark3labs/mcp-go/server"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||||
|
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ToolSwipe implements the generic swipe tool call.
|
// ToolSwipe implements the generic swipe tool call.
|
||||||
@@ -124,15 +125,13 @@ func (t *ToolSwipeDirection) Implement() server.ToolHandlerFunc {
|
|||||||
swipeDirection, validDirections)
|
swipeDirection, validDirections)
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := []option.ActionOption{
|
// Build all options from request arguments
|
||||||
option.WithDuration(getFloat64ValueOrDefault(unifiedReq.Duration, 0.5)),
|
opts := unifiedReq.Options()
|
||||||
option.WithPressDuration(getFloat64ValueOrDefault(unifiedReq.PressDuration, 0.1)),
|
if unifiedReq.Duration == 0 {
|
||||||
|
opts = append(opts, option.WithDuration(0.5))
|
||||||
}
|
}
|
||||||
if unifiedReq.AntiRisk {
|
if unifiedReq.PressDuration == 0 {
|
||||||
opts = append(opts, option.WithAntiRisk(true))
|
opts = append(opts, option.WithPressDuration(0.1))
|
||||||
}
|
|
||||||
if unifiedReq.PreMarkOperation {
|
|
||||||
opts = append(opts, option.WithPreMarkOperation(true))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert direction to coordinates and perform swipe
|
// Convert direction to coordinates and perform swipe
|
||||||
@@ -240,17 +239,8 @@ func (t *ToolSwipeCoordinate) Implement() server.ToolHandlerFunc {
|
|||||||
|
|
||||||
params := []float64{unifiedReq.FromX, unifiedReq.FromY, unifiedReq.ToX, unifiedReq.ToY}
|
params := []float64{unifiedReq.FromX, unifiedReq.FromY, unifiedReq.ToX, unifiedReq.ToY}
|
||||||
|
|
||||||
// Build action options from the unified request
|
// Build all options from request arguments
|
||||||
opts := []option.ActionOption{}
|
opts := unifiedReq.Options()
|
||||||
if unifiedReq.Duration > 0 {
|
|
||||||
opts = append(opts, option.WithDuration(unifiedReq.Duration))
|
|
||||||
}
|
|
||||||
if unifiedReq.PressDuration > 0 {
|
|
||||||
opts = append(opts, option.WithPressDuration(unifiedReq.PressDuration))
|
|
||||||
}
|
|
||||||
if unifiedReq.AntiRisk {
|
|
||||||
opts = append(opts, option.WithAntiRisk(true))
|
|
||||||
}
|
|
||||||
|
|
||||||
swipeAction := prepareSwipeAction(driverExt, params, opts...)
|
swipeAction := prepareSwipeAction(driverExt, params, opts...)
|
||||||
err = swipeAction(driverExt)
|
err = swipeAction(driverExt)
|
||||||
@@ -327,7 +317,7 @@ func (t *ToolSwipeToTapApp) Implement() server.ToolHandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build action options from request structure
|
// Build action options from request structure
|
||||||
var opts []option.ActionOption
|
opts := unifiedReq.Options()
|
||||||
|
|
||||||
// Add boolean options
|
// Add boolean options
|
||||||
if unifiedReq.IgnoreNotFoundError {
|
if unifiedReq.IgnoreNotFoundError {
|
||||||
@@ -400,24 +390,8 @@ func (t *ToolSwipeToTapText) Implement() server.ToolHandlerFunc {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build action options from request structure
|
// Build all options from request arguments
|
||||||
var opts []option.ActionOption
|
opts := unifiedReq.Options()
|
||||||
|
|
||||||
// Add boolean options
|
|
||||||
if unifiedReq.IgnoreNotFoundError {
|
|
||||||
opts = append(opts, option.WithIgnoreNotFoundError(true))
|
|
||||||
}
|
|
||||||
if unifiedReq.Regex {
|
|
||||||
opts = append(opts, option.WithRegex(true))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add numeric options
|
|
||||||
if unifiedReq.MaxRetryTimes > 0 {
|
|
||||||
opts = append(opts, option.WithMaxRetryTimes(unifiedReq.MaxRetryTimes))
|
|
||||||
}
|
|
||||||
if unifiedReq.Index > 0 {
|
|
||||||
opts = append(opts, option.WithIndex(unifiedReq.Index))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Swipe to tap text action logic
|
// Swipe to tap text action logic
|
||||||
err = driverExt.SwipeToTapTexts([]string{unifiedReq.Text}, opts...)
|
err = driverExt.SwipeToTapTexts([]string{unifiedReq.Text}, opts...)
|
||||||
@@ -478,24 +452,8 @@ func (t *ToolSwipeToTapTexts) Implement() server.ToolHandlerFunc {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build action options from request structure
|
// Build all options from request arguments
|
||||||
var opts []option.ActionOption
|
opts := unifiedReq.Options()
|
||||||
|
|
||||||
// Add boolean options
|
|
||||||
if unifiedReq.IgnoreNotFoundError {
|
|
||||||
opts = append(opts, option.WithIgnoreNotFoundError(true))
|
|
||||||
}
|
|
||||||
if unifiedReq.Regex {
|
|
||||||
opts = append(opts, option.WithRegex(true))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add numeric options
|
|
||||||
if unifiedReq.MaxRetryTimes > 0 {
|
|
||||||
opts = append(opts, option.WithMaxRetryTimes(unifiedReq.MaxRetryTimes))
|
|
||||||
}
|
|
||||||
if unifiedReq.Index > 0 {
|
|
||||||
opts = append(opts, option.WithIndex(unifiedReq.Index))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Swipe to tap texts action logic
|
// Swipe to tap texts action logic
|
||||||
err = driverExt.SwipeToTapTexts(unifiedReq.Texts, opts...)
|
err = driverExt.SwipeToTapTexts(unifiedReq.Texts, opts...)
|
||||||
@@ -575,12 +533,10 @@ func (t *ToolDrag) Implement() server.ToolHandlerFunc {
|
|||||||
return nil, fmt.Errorf("from_x, from_y, to_x, and to_y coordinates are required")
|
return nil, fmt.Errorf("from_x, from_y, to_x, and to_y coordinates are required")
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := []option.ActionOption{}
|
// Build all options from request arguments
|
||||||
if unifiedReq.Duration > 0 {
|
opts := unifiedReq.Options()
|
||||||
opts = append(opts, option.WithDuration(unifiedReq.Duration/1000.0))
|
if unifiedReq.Duration == 0 {
|
||||||
}
|
opts = append(opts, option.WithDuration(0.5))
|
||||||
if unifiedReq.AntiRisk {
|
|
||||||
opts = append(opts, option.WithAntiRisk(true))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drag action logic
|
// Drag action logic
|
||||||
|
|||||||
+7
-31
@@ -4,10 +4,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
|
||||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
"github.com/mark3labs/mcp-go/server"
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
|
||||||
|
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||||
|
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ToolTapXY implements the tap_xy tool call.
|
// ToolTapXY implements the tap_xy tool call.
|
||||||
@@ -42,14 +43,9 @@ func (t *ToolTapXY) Implement() server.ToolHandlerFunc {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get options directly since ActionOptions is now ActionOptions
|
// Build all options from request arguments
|
||||||
opts := unifiedReq.Options()
|
opts := unifiedReq.Options()
|
||||||
|
|
||||||
// Add configurable options based on request
|
|
||||||
if unifiedReq.PreMarkOperation {
|
|
||||||
opts = append(opts, option.WithPreMarkOperation(true))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate required parameters
|
// Validate required parameters
|
||||||
if unifiedReq.X == 0 || unifiedReq.Y == 0 {
|
if unifiedReq.X == 0 || unifiedReq.Y == 0 {
|
||||||
return nil, fmt.Errorf("x and y coordinates are required")
|
return nil, fmt.Errorf("x and y coordinates are required")
|
||||||
@@ -123,19 +119,9 @@ func (t *ToolTapAbsXY) Implement() server.ToolHandlerFunc {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get options directly since ActionOptions is now ActionOptions
|
// Build all options from request arguments
|
||||||
opts := unifiedReq.Options()
|
opts := unifiedReq.Options()
|
||||||
|
|
||||||
// Add configurable options based on request
|
|
||||||
if unifiedReq.PreMarkOperation {
|
|
||||||
opts = append(opts, option.WithPreMarkOperation(true))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add AntiRisk support
|
|
||||||
if unifiedReq.AntiRisk {
|
|
||||||
opts = append(opts, option.WithAntiRisk(true))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate required parameters
|
// Validate required parameters
|
||||||
if unifiedReq.X == 0 || unifiedReq.Y == 0 {
|
if unifiedReq.X == 0 || unifiedReq.Y == 0 {
|
||||||
return nil, fmt.Errorf("x and y coordinates are required")
|
return nil, fmt.Errorf("x and y coordinates are required")
|
||||||
@@ -208,14 +194,9 @@ func (t *ToolTapByOCR) Implement() server.ToolHandlerFunc {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get options directly since ActionOptions is now ActionOptions
|
// Build all options from request arguments
|
||||||
opts := unifiedReq.Options()
|
opts := unifiedReq.Options()
|
||||||
|
|
||||||
// Add configurable options based on request
|
|
||||||
if unifiedReq.PreMarkOperation {
|
|
||||||
opts = append(opts, option.WithPreMarkOperation(true))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate required parameters
|
// Validate required parameters
|
||||||
if unifiedReq.Text == "" {
|
if unifiedReq.Text == "" {
|
||||||
return nil, fmt.Errorf("text parameter is required")
|
return nil, fmt.Errorf("text parameter is required")
|
||||||
@@ -277,14 +258,9 @@ func (t *ToolTapByCV) Implement() server.ToolHandlerFunc {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get options directly since ActionOptions is now ActionOptions
|
// Build all options from request arguments
|
||||||
opts := unifiedReq.Options()
|
opts := unifiedReq.Options()
|
||||||
|
|
||||||
// Add configurable options based on request
|
|
||||||
if unifiedReq.PreMarkOperation {
|
|
||||||
opts = append(opts, option.WithPreMarkOperation(true))
|
|
||||||
}
|
|
||||||
|
|
||||||
// For TapByCV, we need to check if there are UI types in the options
|
// For TapByCV, we need to check if there are UI types in the options
|
||||||
// In the original DoAction, it requires ScreenShotWithUITypes to be set
|
// In the original DoAction, it requires ScreenShotWithUITypes to be set
|
||||||
// We'll add a basic implementation that triggers CV recognition
|
// We'll add a basic implementation that triggers CV recognition
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
|
||||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
"github.com/mark3labs/mcp-go/server"
|
"github.com/mark3labs/mcp-go/server"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||||
|
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ToolWebLoginNoneUI implements the web_login_none_ui tool call.
|
// ToolWebLoginNoneUI implements the web_login_none_ui tool call.
|
||||||
@@ -170,7 +171,7 @@ func (t *ToolHoverBySelector) Implement() server.ToolHandlerFunc {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Get options directly since ActionOptions is now ActionOptions
|
// Build all options from request arguments
|
||||||
opts := unifiedReq.Options()
|
opts := unifiedReq.Options()
|
||||||
|
|
||||||
// Hover by selector action logic
|
// Hover by selector action logic
|
||||||
@@ -228,7 +229,7 @@ func (t *ToolTapBySelector) Implement() server.ToolHandlerFunc {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Get options directly since ActionOptions is now ActionOptions
|
// Build all options from request arguments
|
||||||
opts := unifiedReq.Options()
|
opts := unifiedReq.Options()
|
||||||
|
|
||||||
// Tap by selector action logic
|
// Tap by selector action logic
|
||||||
@@ -286,7 +287,7 @@ func (t *ToolSecondaryClickBySelector) Implement() server.ToolHandlerFunc {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Get options directly since ActionOptions is now ActionOptions
|
// Build all options from request arguments
|
||||||
opts := unifiedReq.Options()
|
opts := unifiedReq.Options()
|
||||||
|
|
||||||
// Secondary click by selector action logic
|
// Secondary click by selector action logic
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
|
||||||
"github.com/httprunner/httprunner/v5/uixt/types"
|
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
"github.com/mark3labs/mcp-go/mcp"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||||
|
"github.com/httprunner/httprunner/v5/uixt/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MobileAction struct {
|
type MobileAction struct {
|
||||||
@@ -326,6 +327,10 @@ func (o *ActionOptions) Options() []ActionOption {
|
|||||||
options = append(options, WithAntiRisk(true))
|
options = append(options, WithAntiRisk(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if o.PreMarkOperation {
|
||||||
|
options = append(options, WithPreMarkOperation(true))
|
||||||
|
}
|
||||||
|
|
||||||
// custom options
|
// custom options
|
||||||
if o.Custom != nil {
|
if o.Custom != nil {
|
||||||
for k, v := range o.Custom {
|
for k, v := range o.Custom {
|
||||||
|
|||||||
@@ -152,17 +152,6 @@ func WithDeviceWDAMjpegPort(port int) DeviceOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithDeviceLazySetup(lazySetup bool) DeviceOption {
|
|
||||||
return func(device *DeviceOptions) {
|
|
||||||
if device.IOSDeviceOptions != nil {
|
|
||||||
device.IOSDeviceOptions.LazySetup = lazySetup
|
|
||||||
}
|
|
||||||
if device.Platform == "" {
|
|
||||||
device.Platform = "ios"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithDeviceResetHomeOnStartup(reset bool) DeviceOption {
|
func WithDeviceResetHomeOnStartup(reset bool) DeviceOption {
|
||||||
return func(device *DeviceOptions) {
|
return func(device *DeviceOptions) {
|
||||||
if device.IOSDeviceOptions != nil {
|
if device.IOSDeviceOptions != nil {
|
||||||
|
|||||||
+2
-37
@@ -6,7 +6,6 @@ type IOSDeviceOptions struct {
|
|||||||
WDAPort int `json:"port,omitempty" yaml:"port,omitempty"` // WDA remote port
|
WDAPort int `json:"port,omitempty" yaml:"port,omitempty"` // WDA remote port
|
||||||
WDAMjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` // WDA remote MJPEG port
|
WDAMjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` // WDA remote MJPEG port
|
||||||
LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"`
|
LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"`
|
||||||
LazySetup bool `json:"lazy_setup,omitempty" yaml:"lazy_setup,omitempty"` // lazy setup WDA
|
|
||||||
|
|
||||||
// switch to iOS springboard before init WDA session
|
// switch to iOS springboard before init WDA session
|
||||||
ResetHomeOnStartup bool `json:"reset_home_on_startup,omitempty" yaml:"reset_home_on_startup,omitempty"`
|
ResetHomeOnStartup bool `json:"reset_home_on_startup,omitempty" yaml:"reset_home_on_startup,omitempty"`
|
||||||
@@ -33,9 +32,6 @@ func (dev *IOSDeviceOptions) Options() (deviceOptions []IOSDeviceOption) {
|
|||||||
if dev.LogOn {
|
if dev.LogOn {
|
||||||
deviceOptions = append(deviceOptions, WithWDALogOn(true))
|
deviceOptions = append(deviceOptions, WithWDALogOn(true))
|
||||||
}
|
}
|
||||||
if dev.LazySetup {
|
|
||||||
deviceOptions = append(deviceOptions, WithLazySetup(true))
|
|
||||||
}
|
|
||||||
if dev.ResetHomeOnStartup {
|
if dev.ResetHomeOnStartup {
|
||||||
deviceOptions = append(deviceOptions, WithResetHomeOnStartup(true))
|
deviceOptions = append(deviceOptions, WithResetHomeOnStartup(true))
|
||||||
}
|
}
|
||||||
@@ -52,23 +48,8 @@ func (dev *IOSDeviceOptions) Options() (deviceOptions []IOSDeviceOption) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultWDAPort = 8100
|
defaultWDAPort = 8700
|
||||||
defaultMjpegPort = 9100
|
defaultMjpegPort = 8800
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Changes the value of maximum depth for traversing elements source tree.
|
|
||||||
// It may help to prevent out of memory or timeout errors while getting the elements source tree,
|
|
||||||
// but it might restrict the depth of source tree.
|
|
||||||
// A part of elements source tree might be lost if the value was too small. Defaults to 50
|
|
||||||
defaultSnapshotMaxDepth = 10
|
|
||||||
// Allows to customize accept/dismiss alert button selector.
|
|
||||||
// It helps you to handle an arbitrary element as accept button in accept alert command.
|
|
||||||
// The selector should be a valid class chain expression, where the search root is the alert element itself.
|
|
||||||
// The default button location algorithm is used if the provided selector is wrong or does not match any element.
|
|
||||||
// e.g. **/XCUIElementTypeButton[`label CONTAINS[c] 'accept'`]
|
|
||||||
acceptAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'允许','好','仅在使用应用期间','稍后再说'}`]"
|
|
||||||
dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewIOSDeviceOptions(opts ...IOSDeviceOption) *IOSDeviceOptions {
|
func NewIOSDeviceOptions(opts ...IOSDeviceOption) *IOSDeviceOptions {
|
||||||
@@ -84,16 +65,6 @@ func NewIOSDeviceOptions(opts ...IOSDeviceOption) *IOSDeviceOptions {
|
|||||||
config.WDAMjpegPort = defaultMjpegPort
|
config.WDAMjpegPort = defaultMjpegPort
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.SnapshotMaxDepth == 0 {
|
|
||||||
config.SnapshotMaxDepth = defaultSnapshotMaxDepth
|
|
||||||
}
|
|
||||||
if config.AcceptAlertButtonSelector == "" {
|
|
||||||
config.AcceptAlertButtonSelector = acceptAlertButtonSelector
|
|
||||||
}
|
|
||||||
if config.DismissAlertButtonSelector == "" {
|
|
||||||
config.DismissAlertButtonSelector = dismissAlertButtonSelector
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,12 +100,6 @@ func WithWDALogOn(logOn bool) IOSDeviceOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithLazySetup(lazySetup bool) IOSDeviceOption {
|
|
||||||
return func(device *IOSDeviceOptions) {
|
|
||||||
device.LazySetup = lazySetup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithResetHomeOnStartup(reset bool) IOSDeviceOption {
|
func WithResetHomeOnStartup(reset bool) IOSDeviceOption {
|
||||||
return func(device *IOSDeviceOptions) {
|
return func(device *IOSDeviceOptions) {
|
||||||
device.ResetHomeOnStartup = reset
|
device.ResetHomeOnStartup = reset
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ func (dExt *XTDriver) ExecuteAction(ctx context.Context, action option.MobileAct
|
|||||||
sessionData := dExt.GetSession().GetData(true) // reset after getting data
|
sessionData := dExt.GetSession().GetData(true) // reset after getting data
|
||||||
|
|
||||||
log.Debug().Str("tool", string(tool.Name())).
|
log.Debug().Str("tool", string(tool.Name())).
|
||||||
|
Interface("result", result.Content).
|
||||||
Msg("executed action via MCP tool")
|
Msg("executed action via MCP tool")
|
||||||
return sessionData, nil
|
return sessionData, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ import "fmt"
|
|||||||
type DeviceStatus struct {
|
type DeviceStatus struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
|
Ready bool `json:"ready"`
|
||||||
|
Device string `json:"device"`
|
||||||
OS struct {
|
OS struct {
|
||||||
TestmanagerdVersion int `json:"testmanagerdVersion"`
|
TestmanagerdVersion int `json:"testmanagerdVersion"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -39,10 +41,11 @@ type DeviceStatus struct {
|
|||||||
IP string `json:"ip"`
|
IP string `json:"ip"`
|
||||||
SimulatorVersion string `json:"simulatorVersion"`
|
SimulatorVersion string `json:"simulatorVersion"`
|
||||||
} `json:"ios"`
|
} `json:"ios"`
|
||||||
Ready bool `json:"ready"`
|
|
||||||
Build struct {
|
Build struct {
|
||||||
Time string `json:"time"`
|
Time string `json:"time"`
|
||||||
ProductBundleIdentifier string `json:"productBundleIdentifier"`
|
ProductBundleIdentifier string `json:"productBundleIdentifier"`
|
||||||
|
Version string `json:"version"` // OpenSource WDA version
|
||||||
|
GtfWDAVersion string `json:"gtfWDAVersion"` // GTF WDA version
|
||||||
} `json:"build"`
|
} `json:"build"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user