diff --git a/examples/game/romantic_restaurant/game_romantic_restaurant.json b/examples/game/romantic_restaurant/game_romantic_restaurant.json index 2a5e2654..98540547 100644 --- a/examples/game/romantic_restaurant/game_romantic_restaurant.json +++ b/examples/game/romantic_restaurant/game_romantic_restaurant.json @@ -9,38 +9,17 @@ } }, "teststeps": [ - { - "name": "启动抖音 app", - "android": { - "os_type": "android", - "actions": [ - { - "method": "app_launch", - "params": "$package_name" - }, - { - "method": "sleep", - "params": 5 - } - ] - }, - "validate": [ - { - "check": "ui_foreground_app", - "assert": "equal", - "expect": "$package_name", - "msg": "app [$package_name] should be in foreground" - } - ] - }, { "name": "进入「浪漫餐厅」小游戏", "android": { "os_type": "android", "actions": [ + { + "method": "home" + }, { "method": "start_to_goal", - "params": "搜索「浪漫餐厅」,进入小游戏", + "params": "在手机桌面点击「浪漫餐厅」启动小游戏,等待游戏加载完成", "options": { "pre_mark_operation": true } @@ -63,7 +42,7 @@ "actions": [ { "method": "start_to_goal", - "params": "浪漫餐厅是一款经营类游戏,以下是游戏的基本规则说明:\n1、点击右下角锅铲,开始任务\n2、将棋子拖拽至相同棋子,可升级生成新棋子\n3、拖拽相同棋子时,被部分遮挡的棋子只能作为拖拽终点,不能作为拖拽起点\n4、当游戏界面中没有相同棋子时,可以点击游戏页面中央的购物袋生成新的棋子\n5、若不知道如何操作,请按照游戏指引进行游玩\n\n请严格按照以上游戏规则,开始游戏\n", + "params": "浪漫餐厅是一款经营类游戏,以下是游戏的基本规则说明:\n1、点击右下角锅铲,开始任务\n2、将棋子拖拽至相同棋子,可升级生成新棋子;注意,必须是相同类别和形状的棋子才能合成,例如,长面包和圆面包不能合成,方形蛋糕和三角形蛋糕不能合成\n3、拖拽相同棋子时,被部分遮挡的棋子只能作为拖拽终点,不能作为拖拽起点\n4、当游戏界面中没有相同棋子时,可以点击游戏页面中央的购物袋生成新的棋子\n5、若不知道如何操作,请按照游戏指引进行游玩\n6、不要连续重复上一步操作,合成失败后及时更换策略\n\n请严格按照以上游戏规则,开始游戏\n", "options": { "time_limit": 300, "pre_mark_operation": true diff --git a/examples/game/romantic_restaurant/main_test.go b/examples/game/romantic_restaurant/main_test.go index b028243e..5c44bce6 100644 --- a/examples/game/romantic_restaurant/main_test.go +++ b/examples/game/romantic_restaurant/main_test.go @@ -12,10 +12,11 @@ import ( func TestGameRomanticRestaurant(t *testing.T) { userInstruction := `浪漫餐厅是一款经营类游戏,以下是游戏的基本规则说明: 1、点击右下角锅铲,开始任务 -2、将棋子拖拽至相同棋子,可升级生成新棋子 +2、将棋子拖拽至相同棋子,可升级生成新棋子;注意,必须是相同类别和形状的棋子才能合成,例如,长面包和圆面包不能合成,方形蛋糕和三角形蛋糕不能合成 3、拖拽相同棋子时,被部分遮挡的棋子只能作为拖拽终点,不能作为拖拽起点 4、当游戏界面中没有相同棋子时,可以点击游戏页面中央的购物袋生成新的棋子 5、若不知道如何操作,请按照游戏指引进行游玩 +6、不要连续重复上一步操作,合成失败后及时更换策略 请严格按照以上游戏规则,开始游戏 ` @@ -26,15 +27,22 @@ func TestGameRomanticRestaurant(t *testing.T) { "package_name": "com.ss.android.ugc.aweme", }), TestSteps: []hrp.IStep{ - hrp.NewStep("启动抖音 app"). - Android(). - AppLaunch("$package_name"). - Sleep(5). - Validate(). - AssertAppInForeground("$package_name"), + // hrp.NewStep("启动抖音 app"). + // Android(). + // AppLaunch("$package_name"). + // Sleep(5). + // Validate(). + // AssertAppInForeground("$package_name"), + // hrp.NewStep("进入「浪漫餐厅」小游戏"). + // Android(). + // StartToGoal("搜索「浪漫餐厅」,点击进入「游戏」tab,进入小游戏", + // option.WithPreMarkOperation(true)). + // Validate(). + // AssertAI("当前位于游戏界面"), hrp.NewStep("进入「浪漫餐厅」小游戏"). Android(). - StartToGoal("搜索「浪漫餐厅」,进入小游戏", + Home(). + StartToGoal("在手机桌面点击「浪漫餐厅」启动小游戏,等待游戏加载完成", option.WithPreMarkOperation(true)). Validate(). AssertAI("当前位于游戏界面"), diff --git a/examples/game/sudoku/game_sudoku.json b/examples/game/sudoku/game_sudoku.json index 39c01329..344b9e04 100644 --- a/examples/game/sudoku/game_sudoku.json +++ b/examples/game/sudoku/game_sudoku.json @@ -9,38 +9,17 @@ } }, "teststeps": [ - { - "name": "启动抖音 app", - "android": { - "os_type": "android", - "actions": [ - { - "method": "app_launch", - "params": "$package_name" - }, - { - "method": "sleep", - "params": 5 - } - ] - }, - "validate": [ - { - "check": "ui_foreground_app", - "assert": "equal", - "expect": "$package_name", - "msg": "app [$package_name] should be in foreground" - } - ] - }, { "name": "进入「每天数独」小游戏", "android": { "os_type": "android", "actions": [ + { + "method": "home" + }, { "method": "start_to_goal", - "params": "搜索「每天数独」,进入小游戏", + "params": "在手机桌面点击「每天数独」启动小游戏,等待游戏加载完成\n\n1、点击【开始】按钮开始游戏,进入数独的棋盘界面\n2、若提示「体力不足」,可通过观看广告免费获得体力,观看完成后继续开始游戏\n3、进入棋盘界面后即算作目标达成\n", "options": { "pre_mark_operation": true } @@ -51,8 +30,8 @@ { "check": "ui_ai", "assert": "ai_assert", - "expect": "当前页面底部包含「开始」按钮", - "msg": "assert ai prompt [当前页面底部包含「开始」按钮] failed" + "expect": "当前界面包含网格状的棋盘", + "msg": "assert ai prompt [当前界面包含网格状的棋盘] failed" } ] }, @@ -63,7 +42,7 @@ "actions": [ { "method": "start_to_goal", - "params": "每天数独是一款逻辑推理游戏,玩家需要通过推理来确定黄色方块的所在位置,以下是游戏的基本规则说明:\n1、方块外面的数字代表所在那一行或一列的黄色方块数量。\n2、初始状态为白色方块,选择正确后变为黄色方块,选择错误后变为红底的 X。\n3、如果同一行或列有两个数字,则至少需要一个白底 X 分割它们作为间隔。\n4、如果数字与格子最大数相同时,该列或行必然全都是黄色方块。\n5、只能点击白色方块,不要重复点击同一个方块。\n6、若出现「桌面入口」弹窗则直接关闭。\n\n请严格按照以上游戏规则,开始游戏\n", + "params": "每天数独是一款逻辑推理游戏,玩家需要通过推理来确定黄色方块的所在位置,以下是游戏的基本规则说明:\n1、方块外面的数字代表所在那一行或一列的黄色方块数量。\n2、初始状态为白色方块,选择正确后变为黄色方块,选择错误后变为红底的 X。\n3、如果同一行或列有两个数字,则至少需要一个白底 X 分割它们作为间隔。\n4、如果数字与格子最大数相同时,该列或行必然全都是黄色方块。\n5、只能点击白色方块,不要重复点击同一个方块。\n6、若出现「桌面入口」弹窗则直接关闭。\n7、若游戏失败弹出恢复血量的弹窗,请关闭弹窗重新开始游戏。\n\n请严格按照以上游戏规则,开始游戏\n", "options": { "time_limit": 300, "pre_mark_operation": true diff --git a/examples/game/sudoku/main_test.go b/examples/game/sudoku/main_test.go index 9c68c2b7..4fd287c9 100644 --- a/examples/game/sudoku/main_test.go +++ b/examples/game/sudoku/main_test.go @@ -10,6 +10,12 @@ import ( ) func TestGameSudoku(t *testing.T) { + startGameInstruction := `在手机桌面点击「每天数独」启动小游戏,等待游戏加载完成 + +1、点击【开始】按钮开始游戏,进入数独的棋盘界面 +2、若提示「体力不足」,可通过观看广告免费获得体力,观看完成后继续开始游戏 +3、进入棋盘界面后即算作目标达成 +` userInstruction := `每天数独是一款逻辑推理游戏,玩家需要通过推理来确定黄色方块的所在位置,以下是游戏的基本规则说明: 1、方块外面的数字代表所在那一行或一列的黄色方块数量。 2、初始状态为白色方块,选择正确后变为黄色方块,选择错误后变为红底的 X。 @@ -17,6 +23,7 @@ func TestGameSudoku(t *testing.T) { 4、如果数字与格子最大数相同时,该列或行必然全都是黄色方块。 5、只能点击白色方块,不要重复点击同一个方块。 6、若出现「桌面入口」弹窗则直接关闭。 +7、若游戏失败弹出恢复血量的弹窗,请关闭弹窗重新开始游戏。 请严格按照以上游戏规则,开始游戏 ` @@ -27,18 +34,19 @@ func TestGameSudoku(t *testing.T) { "package_name": "com.ss.android.ugc.aweme", }), TestSteps: []hrp.IStep{ - hrp.NewStep("启动抖音 app"). - Android(). - AppLaunch("$package_name"). - Sleep(5). - Validate(). - AssertAppInForeground("$package_name"), + // hrp.NewStep("启动抖音 app"). + // Android(). + // AppLaunch("$package_name"). + // Sleep(5). + // Validate(). + // AssertAppInForeground("$package_name"), hrp.NewStep("进入「每天数独」小游戏"). Android(). - StartToGoal("搜索「每天数独」,进入小游戏", + Home(). + StartToGoal(startGameInstruction, option.WithPreMarkOperation(true)). Validate(). - AssertAI("当前页面底部包含「开始」按钮"), + AssertAI("当前界面包含网格状的棋盘"), hrp.NewStep("开始游戏"). Android(). StartToGoal(userInstruction, diff --git a/examples/game/yuedongxiaozi/game_yuedongxiaozi.json b/examples/game/yuedongxiaozi/game_yuedongxiaozi.json index a7776210..a26df279 100644 --- a/examples/game/yuedongxiaozi/game_yuedongxiaozi.json +++ b/examples/game/yuedongxiaozi/game_yuedongxiaozi.json @@ -9,38 +9,17 @@ } }, "teststeps": [ - { - "name": "启动抖音 app", - "android": { - "os_type": "android", - "actions": [ - { - "method": "app_launch", - "params": "$package_name" - }, - { - "method": "sleep", - "params": 5 - } - ] - }, - "validate": [ - { - "check": "ui_foreground_app", - "assert": "equal", - "expect": "$package_name", - "msg": "app [$package_name] should be in foreground" - } - ] - }, { "name": "启动「跃动小子」小游戏", "android": { "os_type": "android", "actions": [ + { + "method": "home" + }, { "method": "start_to_goal", - "params": "搜索「跃动小子」,启动小游戏", + "params": "在手机桌面点击「跃动小子」启动小游戏,等待游戏加载完成", "options": { "pre_mark_operation": true } @@ -51,8 +30,8 @@ { "check": "ui_ai", "assert": "ai_assert", - "expect": "当前页面底部包含「领地」「试炼」按钮", - "msg": "assert ai prompt [当前页面底部包含「领地」「试炼」按钮] failed" + "expect": "当前在小游戏页面", + "msg": "assert ai prompt [当前在小游戏页面] failed" } ] }, @@ -63,7 +42,7 @@ "actions": [ { "method": "start_to_goal", - "params": "跃动小子是一款开宝箱类的小游戏,以下是游戏的基本规则说明:\n1、打开宝箱,按照游戏指引进行「出售」或「装备」操作。\n2、请持续推进游戏进程。\n3、屏幕底部的黑白按钮不要进行点击操作。\n\n请严格按照以上游戏规则,开始游戏\n", + "params": "跃动小子是一款开宝箱类的小游戏,以下是游戏的基本规则说明:\n1、打开宝箱,按照游戏指引进行「出售」或「装备」操作。\n2、请持续推进游戏进程。\n3、游戏界面底部的黑白按钮不要进行点击操作。\n\n请严格按照以上游戏规则,开始游戏\n", "options": { "time_limit": 300, "pre_mark_operation": true diff --git a/examples/game/yuedongxiaozi/main_test.go b/examples/game/yuedongxiaozi/main_test.go index 29ac3882..d2ad79a9 100644 --- a/examples/game/yuedongxiaozi/main_test.go +++ b/examples/game/yuedongxiaozi/main_test.go @@ -9,11 +9,11 @@ import ( "github.com/httprunner/httprunner/v5/uixt/option" ) -func TestGameZhuadaE(t *testing.T) { +func TestGameYuedongxiaozi(t *testing.T) { userInstruction := `跃动小子是一款开宝箱类的小游戏,以下是游戏的基本规则说明: 1、打开宝箱,按照游戏指引进行「出售」或「装备」操作。 2、请持续推进游戏进程。 -3、屏幕底部的黑白按钮不要进行点击操作。 +3、游戏界面底部的黑白按钮不要进行点击操作。 请严格按照以上游戏规则,开始游戏 ` @@ -25,18 +25,25 @@ func TestGameZhuadaE(t *testing.T) { "package_name": "com.ss.android.ugc.aweme", }), TestSteps: []hrp.IStep{ - hrp.NewStep("启动抖音 app"). - Android(). - AppLaunch("$package_name"). - Sleep(5). - Validate(). - AssertAppInForeground("$package_name"), + // hrp.NewStep("启动抖音 app"). + // Android(). + // AppLaunch("$package_name"). + // Sleep(5). + // Validate(). + // AssertAppInForeground("$package_name"), + // hrp.NewStep("启动「跃动小子」小游戏"). + // Android(). + // StartToGoal("搜索「跃动小子」,点击「小游戏」tab,进入小游戏", + // option.WithPreMarkOperation(true)). + // Validate(). + // AssertAI("当前在小游戏页面"), hrp.NewStep("启动「跃动小子」小游戏"). Android(). - StartToGoal("搜索「跃动小子」,启动小游戏", + Home(). + StartToGoal("在手机桌面点击「跃动小子」启动小游戏,等待游戏加载完成", option.WithPreMarkOperation(true)). Validate(). - AssertAI("当前页面底部包含「领地」「试炼」按钮"), + AssertAI("当前在小游戏页面"), hrp.NewStep("开始游戏"). Android(). StartToGoal(userInstruction, diff --git a/examples/game/zhuadae/game_zhuadae.json b/examples/game/zhuadae/game_zhuadae.json index 991f6795..d993f23a 100644 --- a/examples/game/zhuadae/game_zhuadae.json +++ b/examples/game/zhuadae/game_zhuadae.json @@ -9,41 +9,24 @@ } }, "teststeps": [ - { - "name": "启动抖音 app", - "android": { - "os_type": "android", - "actions": [ - { - "method": "app_launch", - "params": "$package_name" - }, - { - "method": "sleep", - "params": 5 - } - ] - }, - "validate": [ - { - "check": "ui_foreground_app", - "assert": "equal", - "expect": "$package_name", - "msg": "app [$package_name] should be in foreground" - } - ] - }, { "name": "启动「抓大鹅」小游戏", "android": { "os_type": "android", "actions": [ + { + "method": "home" + }, { "method": "start_to_goal", - "params": "搜索「抓大鹅」,启动小游戏", + "params": "在手机桌面点击「抓大鹅」启动小游戏,处理弹窗,等待游戏加载完成", "options": { "pre_mark_operation": true } + }, + { + "method": "sleep", + "params": 10 } ] }, @@ -51,8 +34,8 @@ { "check": "ui_ai", "assert": "ai_assert", - "expect": "当前页面底部包含「抓大鹅」按钮", - "msg": "assert ai prompt [当前页面底部包含「抓大鹅」按钮] failed" + "expect": "当前页面底部包含「抓大鹅」", + "msg": "assert ai prompt [当前页面底部包含「抓大鹅」] failed" } ] }, @@ -67,6 +50,10 @@ "options": { "pre_mark_operation": true } + }, + { + "method": "sleep", + "params": 10 } ] }, diff --git a/examples/game/zhuadae/main_test.go b/examples/game/zhuadae/main_test.go index bffc5cdf..0a701223 100644 --- a/examples/game/zhuadae/main_test.go +++ b/examples/game/zhuadae/main_test.go @@ -28,22 +28,32 @@ func TestGameZhuadaE(t *testing.T) { "package_name": "com.ss.android.ugc.aweme", }), TestSteps: []hrp.IStep{ - hrp.NewStep("启动抖音 app"). - Android(). - AppLaunch("$package_name"). - Sleep(5). - Validate(). - AssertAppInForeground("$package_name"), + // hrp.NewStep("启动抖音 app"). + // Android(). + // AppLaunch("$package_name"). + // Sleep(5). + // Validate(). + // AssertAppInForeground("$package_name"), + // hrp.NewStep("启动「抓大鹅」小游戏"). + // Android(). + // StartToGoal("搜索「抓大鹅」,启动小游戏", + // option.WithPreMarkOperation(true)). + // Sleep(10). + // Validate(). + // AssertAI("当前页面底部包含「抓大鹅」"), hrp.NewStep("启动「抓大鹅」小游戏"). Android(). - StartToGoal("搜索「抓大鹅」,启动小游戏", + Home(). + StartToGoal("在手机桌面点击「抓大鹅」启动小游戏,处理弹窗,等待游戏加载完成", option.WithPreMarkOperation(true)). + Sleep(10). Validate(). - AssertAI("当前页面底部包含「抓大鹅」按钮"), + AssertAI("当前页面底部包含「抓大鹅」"), hrp.NewStep("进入「抓大鹅」小游戏"). Android(). StartToGoal("点击「抓大鹅」,进入小游戏", option.WithPreMarkOperation(true)). + Sleep(10). Validate(). AssertAI("当前页面底部包含「移出」「凑齐」「打乱」按钮"), hrp.NewStep("开始游戏"). diff --git a/internal/version/VERSION b/internal/version/VERSION index 0e7166d7..552c255e 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-250704 +v5.0.0-250710 diff --git a/parser.go b/parser.go index 6989242a..a7021f15 100644 --- a/parser.go +++ b/parser.go @@ -11,13 +11,13 @@ import ( "strconv" "strings" + "github.com/httprunner/funplugin" + "github.com/httprunner/funplugin/fungo" "github.com/maja42/goval" "github.com/mark3labs/mcp-go/mcp" "github.com/pkg/errors" "github.com/rs/zerolog/log" - "github.com/httprunner/funplugin" - "github.com/httprunner/funplugin/fungo" "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/builtin" "github.com/httprunner/httprunner/v5/mcphost" @@ -238,7 +238,7 @@ func (p *Parser) ParseString(raw string, variablesMapping map[string]interface{} log.Debug(). Str("parsedString", parsedString). Int("matchStartPosition", matchStartPosition). - Msg("[parseString] parse function") + Msg("parse string function") continue } @@ -268,7 +268,7 @@ func (p *Parser) ParseString(raw string, variablesMapping map[string]interface{} log.Debug(). Str("parsedString", parsedString). Int("matchStartPosition", matchStartPosition). - Msg("[parseString] parse variable") + Msg("parse string variable") continue } @@ -306,7 +306,8 @@ func (p *Parser) CallFunc(funcName string, arguments ...interface{}) (interface{ // CallMCPTool calls a MCP tool on a specific MCP server func (p *Parser) CallMCPTool(ctx context.Context, serverName, - funcName string, arguments map[string]interface{}) (interface{}, error) { + funcName string, arguments map[string]interface{}, +) (interface{}, error) { if p.MCPHost == nil { return nil, fmt.Errorf("mcphost is not initialized") } diff --git a/report.go b/report.go index 26e78bab..18b44d5f 100644 --- a/report.go +++ b/report.go @@ -1838,6 +1838,101 @@ const htmlTemplate = ` word-wrap: break-word; } + /* AI Assertion Styles */ + .ai-assertion-section { + margin-top: 15px; + padding: 15px; + background: linear-gradient(135deg, #f0f8ff 0%, #f5f5ff 100%); + border: 2px solid #4169e1; + border-radius: 12px; + box-shadow: 0 4px 8px rgba(65, 105, 225, 0.15); + } + + .ai-assertion-section h5 { + margin: 0 0 15px 0; + color: #4169e1; + font-size: 1.1em; + font-weight: 600; + } + + .ai-screenshot-container { + background: white; + border: 1px solid #dee2e6; + border-radius: 8px; + padding: 12px; + margin-bottom: 15px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + .ai-screenshot { + text-align: center; + margin-top: 10px; + } + + .ai-screenshot img { + max-width: 100%; + height: auto; + border-radius: 8px; + border: 1px solid #dee2e6; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + cursor: pointer; + transition: transform 0.2s; + } + + .ai-screenshot img:hover { + transform: scale(1.02); + } + + .ai-analysis-container { + background: white; + border: 1px solid #dee2e6; + border-radius: 8px; + padding: 12px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + .ai-analysis-content { + margin-top: 10px; + } + + .ai-thought { + background: linear-gradient(135deg, #e8f4fd 0%, #f0f8ff 100%); + border: 1px solid #4169e1; + border-radius: 8px; + padding: 12px; + margin: 10px 0; + color: #2c3e50; + } + + .ai-thought .thought-content { + margin-top: 8px; + font-style: italic; + color: #34495e; + white-space: pre-wrap; + word-wrap: break-word; + } + + .ai-raw-response { + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 8px; + padding: 12px; + margin: 10px 0; + color: #2c3e50; + } + + .ai-raw-response .response-content { + margin-top: 8px; + font-family: monospace; + font-size: 0.9em; + background: white; + padding: 8px; + border-radius: 4px; + border: 1px solid #e9ecef; + white-space: pre-wrap; + word-wrap: break-word; + } + @media screen and (max-width: 768px) { .validator-ai-layout { flex-direction: column; @@ -2950,6 +3045,60 @@ const htmlTemplate = ` {{if and $validator.msg (ne $validator.check_result "pass")}}
{{end}} + + + {{if $validator.ai_result}} +