Merge branch 'dev' into 'master'

release v5.0.0

See merge request iesqa/httprunner!107
This commit is contained in:
李隆
2025-06-27 04:15:15 +00:00
56 changed files with 1183 additions and 419 deletions

View File

@@ -36,6 +36,7 @@ var (
genHTMLReport bool
caseTimeout float32
runMCPConfigPath string // MCP config path for run command
autoPopupHandler bool // enable auto popup handler for all steps
)
func init() {
@@ -48,6 +49,7 @@ func init() {
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().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 {
@@ -76,5 +78,8 @@ func makeHRPRunner() *hrp.HRPRunner {
if runMCPConfigPath != "" {
runner.SetMCPConfigPath(runMCPConfigPath)
}
if autoPopupHandler {
runner.EnableAutoPopupHandler(autoPopupHandler)
}
return runner
}

View File

@@ -63,4 +63,4 @@ Copyright © 2017-present debugtalk. Apache-2.0 License.
* [hrp startproject](hrp_startproject.md) - Create a scaffold project
* [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

View File

@@ -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 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

View File

@@ -24,4 +24,4 @@ hrp adb devices [flags]
* [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

View File

@@ -28,4 +28,4 @@ hrp adb install [flags] PACKAGE
* [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

View File

@@ -25,4 +25,4 @@ hrp adb screencap [flags]
* [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

View File

@@ -36,4 +36,4 @@ hrp build $path ... [flags]
* [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

View File

@@ -34,4 +34,4 @@ hrp convert $path... [flags]
* [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

View File

@@ -29,4 +29,4 @@ simple utils for ios device management
* [hrp ios uninstall](hrp_ios_uninstall.md) - uninstall package automatically
* [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

View File

@@ -26,4 +26,4 @@ hrp ios apps [flags]
* [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

View File

@@ -24,4 +24,4 @@ hrp ios devices [flags]
* [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

View File

@@ -25,4 +25,4 @@ hrp ios install [flags] PACKAGE
* [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

View File

@@ -28,4 +28,4 @@ hrp ios mount [flags]
* [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

View File

@@ -26,4 +26,4 @@ hrp ios ps [flags]
* [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

View File

@@ -25,4 +25,4 @@ hrp ios reboot [flags]
* [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

View File

@@ -24,4 +24,4 @@ hrp ios tunnel [flags]
* [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

View File

@@ -26,4 +26,4 @@ hrp ios uninstall [flags] PACKAGE
* [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

View File

@@ -28,4 +28,4 @@ hrp ios xctest [flags]
* [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

View File

@@ -28,4 +28,4 @@ hrp mcp-server [flags]
* [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

View File

@@ -31,4 +31,4 @@ hrp mcphost [flags]
* [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

View File

@@ -24,4 +24,4 @@ hrp pytest $path ... [flags]
* [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

View File

@@ -33,4 +33,4 @@ hrp report [result_folder] [flags]
* [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

View File

@@ -21,16 +21,17 @@ hrp run $path... [flags]
### Options
```
--case-timeout float32 set testcase timeout (seconds) (default 3600)
-c, --continue-on-failure continue running next step when failure occurs
-g, --gen-html-report generate html report
-h, --help help for run
--http-stat turn on HTTP latency stat (DNSLookup, TCP Connection, etc.)
--log-plugin turn on plugin logging
--log-requests-off turn off request & response details logging
--mcp-config string path to the MCP config file
-p, --proxy-url string set proxy url
-s, --save-tests save tests summary
--case-timeout float32 set testcase timeout (seconds) (default 3600)
-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
-h, --help help for run
--http-stat turn on HTTP latency stat (DNSLookup, TCP Connection, etc.)
--log-plugin turn on plugin logging
--log-requests-off turn off request & response details logging
--mcp-config string path to the MCP config file
-p, --proxy-url string set proxy url
-s, --save-tests save tests summary
```
### Options inherited from parent commands
@@ -45,4 +46,4 @@ hrp run $path... [flags]
* [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

View File

@@ -30,4 +30,4 @@ hrp server start [flags]
* [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

View File

@@ -29,4 +29,4 @@ hrp startproject $project_name [flags]
* [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

View File

@@ -24,4 +24,4 @@ hrp wiki [flags]
* [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

View File

@@ -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
}
}
]
}
}
]
}

View File

@@ -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)
}

View File

@@ -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
}
}
]
}
}
]
}

View File

@@ -7,6 +7,7 @@ import (
"os"
"testing"
hrp "github.com/httprunner/httprunner/v5"
"github.com/httprunner/httprunner/v5/internal/builtin"
"github.com/httprunner/httprunner/v5/uixt/ai"
"github.com/httprunner/httprunner/v5/uixt/option"
@@ -15,6 +16,45 @@ import (
"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
func convertToGameElementFromQueryResult(result *ai.QueryResult) (*GameElement, error) {
if result == nil {

View File

@@ -6,16 +6,9 @@
},
"ios": [
{
"perf_options": {
"sys_cpu": true,
"sys_mem": true,
"fps": true,
"network": true
},
"port": 8700,
"mjpeg_port": 8800,
"log_on": true,
"xctest_bundle_id": "com.gtf.wda.runner.xctrunner"
"log_on": true
}
]
},
@@ -23,6 +16,7 @@
{
"name": "启动抖音",
"ios": {
"os_type": "ios",
"actions": [
{
"method": "home"
@@ -34,8 +28,10 @@
{
"method": "swipe_to_tap_app",
"params": "$app_name",
"identifier": "启动抖音",
"max_retry_times": 5
"options": {
"identifier": "启动抖音",
"max_retry_times": 5
}
},
{
"method": "sleep",
@@ -55,11 +51,14 @@
{
"name": "处理青少年弹窗",
"ios": {
"os_type": "ios",
"actions": [
{
"method": "tap_ocr",
"params": "我知道了",
"ignore_NotFoundError": true
"options": {
"ignore_NotFoundError": true
}
}
]
}
@@ -67,11 +66,14 @@
{
"name": "进入购物页",
"ios": {
"os_type": "ios",
"actions": [
{
"method": "tap_ocr",
"params": "商城",
"identifier": "点击商城"
"options": {
"identifier": "点击商城"
}
},
{
"method": "sleep",
@@ -83,11 +85,14 @@
{
"name": "进入推荐页",
"ios": {
"os_type": "ios",
"actions": [
{
"method": "tap_ocr",
"params": "推荐",
"identifier": "点击推荐"
"options": {
"identifier": "点击推荐"
}
},
{
"method": "sleep",
@@ -99,29 +104,36 @@
{
"name": "向上滑动 2 次",
"ios": {
"os_type": "ios",
"actions": [
{
"method": "swipe",
"method": "swipe_direction",
"params": "up",
"identifier": "第 1 次上划"
"options": {
"identifier": "第 1 次上划"
}
},
{
"method": "sleep",
"params": 2
},
{
"method": "swipe",
"method": "swipe_direction",
"params": "up",
"identifier": "第 2 次上划"
"options": {
"identifier": "第 2 次上划"
}
},
{
"method": "sleep",
"params": 2
},
{
"method": "swipe",
"method": "swipe_direction",
"params": "up",
"identifier": "第 3 次上划"
"options": {
"identifier": "第 3 次上划"
}
},
{
"method": "sleep",
@@ -133,7 +145,9 @@
0.9,
0.1
],
"identifier": "点击进入搜索框"
"options": {
"identifier": "点击进入搜索框"
}
},
{
"method": "sleep",
@@ -141,8 +155,17 @@
},
{
"method": "input",
"params": "httprunner",
"identifier": "输入搜索关键词"
"params": "httprunner 发版记录",
"options": {
"identifier": "输入搜索关键词"
}
},
{
"method": "tap_ocr",
"params": "搜索",
"options": {
"identifier": "点击搜索"
}
}
]
}

View File

@@ -43,7 +43,8 @@ func TestWDALog(t *testing.T) {
SwipeUp(option.WithIdentifier("第 2 次上划")).Sleep(2).
SwipeUp(option.WithIdentifier("第 3 次上划")).Sleep(2).
TapXY(0.9, 0.1, option.WithIdentifier("点击进入搜索框")).Sleep(2).
Input("httprunner", option.WithIdentifier("输入搜索关键词")),
Input("httprunner 发版记录", option.WithIdentifier("输入搜索关键词")).
TapByOCR("搜索", option.WithIdentifier("点击搜索")),
},
}

View File

@@ -1 +1 @@
v5.0.0-beta-2506261443
v5.0.0-250627

828
report.go

File diff suppressed because it is too large Load Diff

View File

@@ -18,12 +18,12 @@ import (
"time"
"github.com/gorilla/websocket"
"github.com/httprunner/funplugin"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"golang.org/x/net/http2"
"github.com/httprunner/funplugin"
"github.com/httprunner/httprunner/v5/code"
"github.com/httprunner/httprunner/v5/internal/builtin"
"github.com/httprunner/httprunner/v5/internal/config"
@@ -50,10 +50,11 @@ func NewRunner(t *testing.T) *HRPRunner {
interruptSignal := make(chan os.Signal, 1)
signal.Notify(interruptSignal, syscall.SIGTERM, syscall.SIGINT)
return &HRPRunner{
t: t,
failfast: true, // default to failfast
genHTMLReport: false,
mcpConfigPath: "",
t: t,
failfast: true, // default to failfast
genHTMLReport: false,
mcpConfigPath: "",
autoPopupHandler: false,
httpClient: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
@@ -87,6 +88,7 @@ type HRPRunner struct {
saveTests bool
genHTMLReport bool
mcpConfigPath string // MCP config file path
autoPopupHandler bool // enable auto popup handler for all UI steps
httpClient *http.Client
http2Client *http.Client
wsDialer *websocket.Dialer
@@ -191,8 +193,17 @@ func (r *HRPRunner) SetSaveTests(saveTests bool) *HRPRunner {
// GenHTMLReport configures whether to gen html report of api tests.
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.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
}
@@ -392,6 +403,13 @@ func NewCaseRunner(testcase TestCase, hrpRunner *HRPRunner) (*CaseRunner, error)
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
if parsedConfig.RequestTimeout != 0 {
hrpRunner.SetRequestTimeout(parsedConfig.RequestTimeout)
@@ -855,8 +873,6 @@ func (r *SessionRunner) RunStep(step IStep) (stepResult *StepResult, err error)
stepName := step.Name()
stepType := string(step.Type())
log.Info().Str("step", stepName).Str("type", stepType).Msg(RUN_STEP_START)
// execute step with parameters iterator
tasks, err := r.generateExecutionTasks(step)
if err != nil {
@@ -897,6 +913,8 @@ func (r *SessionRunner) RunStep(step IStep) (stepResult *StepResult, err error)
// execute with loops as outer iteration
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
select {
case <-r.caseRunner.hrpRunner.interruptSignal:

View File

@@ -11,7 +11,7 @@ PRE_COMMIT_FILE=.git/hooks/pre-commit
# install pre-commit hook and make it executable
function install() {
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'
#!/bin/bash
@@ -45,7 +45,7 @@ done
version_file=internal/version/VERSION
# get current date
current_date=$(date +"%y%m%d%H%M")
current_date=$(date +"%y%m%d")
# update version
sed -i '' "s/[0-9]\{10\}/${current_date}/" "$version_file"

View File

@@ -765,6 +765,10 @@ func runStepMobileUI(s *SessionRunner, step IStep) (stepResult *StepResult, err
identifier = action.Identifier
break
}
if action.Options != nil && action.Options.Identifier != "" {
identifier = action.Options.Identifier
break
}
}
}
stepResult.Identifier = identifier

View File

@@ -8,7 +8,6 @@ import (
hrp "github.com/httprunner/httprunner/v5"
"github.com/httprunner/httprunner/v5/uixt/option"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// GameInfo 定义游戏界面分析的输出格式
@@ -111,79 +110,6 @@ func TestAndroidAction(t *testing.T) {
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) {
testCase := &hrp.TestCase{
Config: hrp.NewConfig("run ui action with ai").

View File

@@ -135,9 +135,17 @@ func (p *Planner) Plan(ctx context.Context, opts *PlanningOptions) (result *Plan
for _, toolCall := range message.ToolCalls {
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{
Role: schema.Tool,
Content: message.Content,
Content: toolContent,
ToolCalls: message.ToolCalls,
ToolCallID: toolCallID,
})
@@ -158,7 +166,7 @@ func (p *Planner) Plan(ctx context.Context, opts *PlanningOptions) (result *Plan
Error: err.Error(),
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)
p.history.Append(&schema.Message{

View File

@@ -731,11 +731,21 @@ func (ad *ADBDriver) ForegroundInfo() (app types.AppInfo, err error) {
if err != nil {
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 {
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 {

View File

@@ -424,6 +424,9 @@ func resizeImage(src image.Image, width, height int) image.Image {
// CompressImageFile compresses an image file and returns the compressed data
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
file, err := os.Open(imagePath)
if err != nil {

View File

@@ -87,6 +87,7 @@ func (s *DriverSession) GetData(withReset bool) SessionData {
}
func (s *DriverSession) SetBaseURL(baseUrl string) {
log.Info().Str("baseUrl", baseUrl).Msg("set driver session base URL")
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) (
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)
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)
// Try to reset the session for the next attempt
if s.resetFn != nil {
log.Warn().Msg("reset driver session")
if err2 := s.resetFn(); err2 != nil {
log.Error().Err(err2).Msgf(
"failed to reset session, try count %v", count)
log.Warn().Msgf("attempting to reset driver session before attempt %d", attempt+1)
if resetErr := s.resetFn(); resetErr != nil {
log.Error().Err(resetErr).Msgf("failed to reset session, will retry without reset")
} else {
log.Info().Msgf(
"reset session success, try count %v", count)
log.Info().Msg("session reset successful")
}
}
}
return
return nil, lastError
}
func (s *DriverSession) Request(method string, urlStr string, rawBody []byte) (

View File

@@ -3,7 +3,6 @@
package uixt
import (
"fmt"
"testing"
"github.com/httprunner/httprunner/v5/uixt/option"
@@ -25,7 +24,7 @@ func TestWindowSize(t *testing.T) {
driver := setupHDCDriverExt(t)
size, err := driver.WindowSize()
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) {

View File

@@ -164,6 +164,9 @@ func (dev *IOSDevice) Setup() error {
dev.DeviceEntry.UserspaceTUNPort = info.UserspaceTUNPort
dev.DeviceEntry.UserspaceTUN = info.UserspaceTUN
rsdService, err := ios.NewWithAddrPortDevice(info.Address, info.RsdPort, dev.DeviceEntry)
if err != nil {
return err
}
defer rsdService.Close()
rsdProvider, err := rsdService.Handshake()
if err != nil {
@@ -218,14 +221,6 @@ func (dev *IOSDevice) NewDriver() (driver IDriver, err error) {
if err != nil {
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 {
log.Info().Msg("go back to home screen")

View File

@@ -36,13 +36,19 @@ func NewWDADriver(device *IOSDevice) (*WDADriver, error) {
Session: NewDriverSession(),
}
if !device.Options.LazySetup {
// setup driver
if err := driver.Setup(); err != nil {
return nil, err
}
// setup driver
if err := driver.Setup(); err != nil {
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
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:)]
data := map[string]interface{}{"value": strings.Split(text, "")}
option.MergeOptions(data, opts...)
_, err = wd.Session.POST(data, "/gtf/interaction/input")
_, err = wd.Session.POST(data, "/wings/interaction/keys")
return
}

View File

@@ -38,8 +38,7 @@ func TestDevice_IOS_Install(t *testing.T) {
func TestDriver_WDA_LazySetup(t *testing.T) {
device, err := NewIOSDevice(
option.WithWDAPort(8700),
option.WithWDAMjpegPort(8800),
option.WithLazySetup(true))
option.WithWDAMjpegPort(8800))
require.Nil(t, err)
driver, err := NewWDADriver(device)
require.Nil(t, err)

View File

@@ -193,6 +193,8 @@ func extractActionOptionsToArguments(actionOptions []option.ActionOption, argume
"tap_random_rect": tempOptions.TapRandomRect,
"anti_risk": tempOptions.AntiRisk,
"pre_mark_operation": tempOptions.PreMarkOperation,
"reset_history": tempOptions.ResetHistory,
"match_one": tempOptions.MatchOne,
}
// Add boolean options only if they are true
@@ -209,6 +211,18 @@ func extractActionOptionsToArguments(actionOptions []option.ActionOption, argume
if tempOptions.Index != 0 {
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)
if tempOptions.Duration > 0 {
if _, exists := arguments["duration"]; !exists {
@@ -288,13 +302,19 @@ func extractActionOptionsToArguments(actionOptions []option.ActionOption, argume
if tempOptions.Selector != "" {
arguments["selector"] = tempOptions.Selector
}
}
func getFloat64ValueOrDefault(value float64, defaultValue float64) float64 {
if value == 0 {
return defaultValue
if tempOptions.Identifier != "" {
arguments["identifier"] = tempOptions.Identifier
}
// Add direction option (can be string or []float64)
if tempOptions.Direction != nil {
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

View File

@@ -4,10 +4,11 @@ import (
"context"
"fmt"
"github.com/httprunner/httprunner/v5/uixt/option"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v5/uixt/option"
)
// ToolStartToGoal implements the start_to_goal tool call.
@@ -162,7 +163,7 @@ func (t *ToolAIQuery) Implement() server.ToolHandlerFunc {
return nil, err
}
// Build action options from unified request
// Build all options from request arguments
opts := unifiedReq.Options()
// AI query logic with options

View File

@@ -5,11 +5,12 @@ import (
"fmt"
"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/server"
"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.
@@ -124,15 +125,13 @@ func (t *ToolSwipeDirection) Implement() server.ToolHandlerFunc {
swipeDirection, validDirections)
}
opts := []option.ActionOption{
option.WithDuration(getFloat64ValueOrDefault(unifiedReq.Duration, 0.5)),
option.WithPressDuration(getFloat64ValueOrDefault(unifiedReq.PressDuration, 0.1)),
// Build all options from request arguments
opts := unifiedReq.Options()
if unifiedReq.Duration == 0 {
opts = append(opts, option.WithDuration(0.5))
}
if unifiedReq.AntiRisk {
opts = append(opts, option.WithAntiRisk(true))
}
if unifiedReq.PreMarkOperation {
opts = append(opts, option.WithPreMarkOperation(true))
if unifiedReq.PressDuration == 0 {
opts = append(opts, option.WithPressDuration(0.1))
}
// 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}
// Build action options from the unified request
opts := []option.ActionOption{}
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))
}
// Build all options from request arguments
opts := unifiedReq.Options()
swipeAction := prepareSwipeAction(driverExt, params, opts...)
err = swipeAction(driverExt)
@@ -327,7 +317,7 @@ func (t *ToolSwipeToTapApp) Implement() server.ToolHandlerFunc {
}
// Build action options from request structure
var opts []option.ActionOption
opts := unifiedReq.Options()
// Add boolean options
if unifiedReq.IgnoreNotFoundError {
@@ -400,24 +390,8 @@ func (t *ToolSwipeToTapText) Implement() server.ToolHandlerFunc {
return nil, err
}
// Build action options from request structure
var opts []option.ActionOption
// 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))
}
// Build all options from request arguments
opts := unifiedReq.Options()
// Swipe to tap text action logic
err = driverExt.SwipeToTapTexts([]string{unifiedReq.Text}, opts...)
@@ -478,24 +452,8 @@ func (t *ToolSwipeToTapTexts) Implement() server.ToolHandlerFunc {
return nil, err
}
// Build action options from request structure
var opts []option.ActionOption
// 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))
}
// Build all options from request arguments
opts := unifiedReq.Options()
// Swipe to tap texts action logic
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")
}
opts := []option.ActionOption{}
if unifiedReq.Duration > 0 {
opts = append(opts, option.WithDuration(unifiedReq.Duration/1000.0))
}
if unifiedReq.AntiRisk {
opts = append(opts, option.WithAntiRisk(true))
// Build all options from request arguments
opts := unifiedReq.Options()
if unifiedReq.Duration == 0 {
opts = append(opts, option.WithDuration(0.5))
}
// Drag action logic

View File

@@ -4,10 +4,11 @@ import (
"context"
"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/server"
"github.com/httprunner/httprunner/v5/internal/builtin"
"github.com/httprunner/httprunner/v5/uixt/option"
)
// ToolTapXY implements the tap_xy tool call.
@@ -42,14 +43,9 @@ func (t *ToolTapXY) Implement() server.ToolHandlerFunc {
return nil, err
}
// Get options directly since ActionOptions is now ActionOptions
// Build all options from request arguments
opts := unifiedReq.Options()
// Add configurable options based on request
if unifiedReq.PreMarkOperation {
opts = append(opts, option.WithPreMarkOperation(true))
}
// Validate required parameters
if unifiedReq.X == 0 || unifiedReq.Y == 0 {
return nil, fmt.Errorf("x and y coordinates are required")
@@ -123,19 +119,9 @@ func (t *ToolTapAbsXY) Implement() server.ToolHandlerFunc {
return nil, err
}
// Get options directly since ActionOptions is now ActionOptions
// Build all options from request arguments
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
if unifiedReq.X == 0 || unifiedReq.Y == 0 {
return nil, fmt.Errorf("x and y coordinates are required")
@@ -208,14 +194,9 @@ func (t *ToolTapByOCR) Implement() server.ToolHandlerFunc {
return nil, err
}
// Get options directly since ActionOptions is now ActionOptions
// Build all options from request arguments
opts := unifiedReq.Options()
// Add configurable options based on request
if unifiedReq.PreMarkOperation {
opts = append(opts, option.WithPreMarkOperation(true))
}
// Validate required parameters
if unifiedReq.Text == "" {
return nil, fmt.Errorf("text parameter is required")
@@ -277,14 +258,9 @@ func (t *ToolTapByCV) Implement() server.ToolHandlerFunc {
return nil, err
}
// Get options directly since ActionOptions is now ActionOptions
// Build all options from request arguments
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
// In the original DoAction, it requires ScreenShotWithUITypes to be set
// We'll add a basic implementation that triggers CV recognition

View File

@@ -5,11 +5,12 @@ import (
"encoding/json"
"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/server"
"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.
@@ -170,7 +171,7 @@ func (t *ToolHoverBySelector) Implement() server.ToolHandlerFunc {
if err != nil {
return nil, err
}
// Get options directly since ActionOptions is now ActionOptions
// Build all options from request arguments
opts := unifiedReq.Options()
// Hover by selector action logic
@@ -228,7 +229,7 @@ func (t *ToolTapBySelector) Implement() server.ToolHandlerFunc {
if err != nil {
return nil, err
}
// Get options directly since ActionOptions is now ActionOptions
// Build all options from request arguments
opts := unifiedReq.Options()
// Tap by selector action logic
@@ -286,7 +287,7 @@ func (t *ToolSecondaryClickBySelector) Implement() server.ToolHandlerFunc {
if err != nil {
return nil, err
}
// Get options directly since ActionOptions is now ActionOptions
// Build all options from request arguments
opts := unifiedReq.Options()
// Secondary click by selector action logic

View File

@@ -7,10 +7,11 @@ import (
"reflect"
"strings"
"github.com/httprunner/httprunner/v5/internal/builtin"
"github.com/httprunner/httprunner/v5/uixt/types"
"github.com/mark3labs/mcp-go/mcp"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v5/internal/builtin"
"github.com/httprunner/httprunner/v5/uixt/types"
)
type MobileAction struct {
@@ -326,6 +327,10 @@ func (o *ActionOptions) Options() []ActionOption {
options = append(options, WithAntiRisk(true))
}
if o.PreMarkOperation {
options = append(options, WithPreMarkOperation(true))
}
// custom options
if o.Custom != nil {
for k, v := range o.Custom {

View File

@@ -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 {
return func(device *DeviceOptions) {
if device.IOSDeviceOptions != nil {

View File

@@ -6,7 +6,6 @@ type IOSDeviceOptions struct {
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
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
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 {
deviceOptions = append(deviceOptions, WithWDALogOn(true))
}
if dev.LazySetup {
deviceOptions = append(deviceOptions, WithLazySetup(true))
}
if dev.ResetHomeOnStartup {
deviceOptions = append(deviceOptions, WithResetHomeOnStartup(true))
}
@@ -52,23 +48,8 @@ func (dev *IOSDeviceOptions) Options() (deviceOptions []IOSDeviceOption) {
}
const (
defaultWDAPort = 8100
defaultMjpegPort = 9100
)
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 {'不允许','暂不'}`]"
defaultWDAPort = 8700
defaultMjpegPort = 8800
)
func NewIOSDeviceOptions(opts ...IOSDeviceOption) *IOSDeviceOptions {
@@ -84,16 +65,6 @@ func NewIOSDeviceOptions(opts ...IOSDeviceOption) *IOSDeviceOptions {
config.WDAMjpegPort = defaultMjpegPort
}
if config.SnapshotMaxDepth == 0 {
config.SnapshotMaxDepth = defaultSnapshotMaxDepth
}
if config.AcceptAlertButtonSelector == "" {
config.AcceptAlertButtonSelector = acceptAlertButtonSelector
}
if config.DismissAlertButtonSelector == "" {
config.DismissAlertButtonSelector = dismissAlertButtonSelector
}
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 {
return func(device *IOSDeviceOptions) {
device.ResetHomeOnStartup = reset

View File

@@ -147,6 +147,7 @@ func (dExt *XTDriver) ExecuteAction(ctx context.Context, action option.MobileAct
sessionData := dExt.GetSession().GetData(true) // reset after getting data
log.Debug().Str("tool", string(tool.Name())).
Interface("result", result.Content).
Msg("executed action via MCP tool")
return sessionData, nil
}

View File

@@ -29,6 +29,8 @@ import "fmt"
type DeviceStatus struct {
Message string `json:"message"`
State string `json:"state"`
Ready bool `json:"ready"`
Device string `json:"device"`
OS struct {
TestmanagerdVersion int `json:"testmanagerdVersion"`
Name string `json:"name"`
@@ -39,10 +41,11 @@ type DeviceStatus struct {
IP string `json:"ip"`
SimulatorVersion string `json:"simulatorVersion"`
} `json:"ios"`
Ready bool `json:"ready"`
Build struct {
Time string `json:"time"`
ProductBundleIdentifier string `json:"productBundleIdentifier"`
Version string `json:"version"` // OpenSource WDA version
GtfWDAVersion string `json:"gtfWDAVersion"` // GTF WDA version
} `json:"build"`
}