mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-11 18:11:21 +08:00
merge master
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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("当前位于游戏界面"),
|
||||
|
||||
@@ -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、只能点击白色方块,不要重复点击同一个方块。\n\n请严格按照以上游戏规则,开始游戏\n",
|
||||
"params": "每天数独是一款逻辑推理游戏,玩家需要通过推理来确定黄色方块的所在位置,以下是游戏的基本规则说明:\n1、方块外面的数字代表所在那一行或一列的黄色方块数量。\n2、初始状态为白色方块,选择正确后变为黄色方块,选择错误后变为红底的 X。\n3、如果同一行或列有两个数字,则至少需要一个白底 X 分割它们作为间隔。\n4、如果数字与格子最大数相同时,该列或行必然全都是黄色方块。\n5、只能点击白色方块,不要重复点击同一个方块。\n6、若出现「桌面入口」弹窗则直接关闭。\n7、若游戏失败弹出恢复血量的弹窗,请关闭弹窗重新开始游戏。\n\n请严格按照以上游戏规则,开始游戏\n",
|
||||
"options": {
|
||||
"time_limit": 300,
|
||||
"pre_mark_operation": true
|
||||
|
||||
@@ -10,12 +10,20 @@ import (
|
||||
)
|
||||
|
||||
func TestGameSudoku(t *testing.T) {
|
||||
startGameInstruction := `在手机桌面点击「每天数独」启动小游戏,等待游戏加载完成
|
||||
|
||||
1、点击【开始】按钮开始游戏,进入数独的棋盘界面
|
||||
2、若提示「体力不足」,可通过观看广告免费获得体力,观看完成后继续开始游戏
|
||||
3、进入棋盘界面后即算作目标达成
|
||||
`
|
||||
userInstruction := `每天数独是一款逻辑推理游戏,玩家需要通过推理来确定黄色方块的所在位置,以下是游戏的基本规则说明:
|
||||
1、方块外面的数字代表所在那一行或一列的黄色方块数量。
|
||||
2、初始状态为白色方块,选择正确后变为黄色方块,选择错误后变为红底的 X。
|
||||
3、如果同一行或列有两个数字,则至少需要一个白底 X 分割它们作为间隔。
|
||||
4、如果数字与格子最大数相同时,该列或行必然全都是黄色方块。
|
||||
5、只能点击白色方块,不要重复点击同一个方块。
|
||||
6、若出现「桌面入口」弹窗则直接关闭。
|
||||
7、若游戏失败弹出恢复血量的弹窗,请关闭弹窗重新开始游戏。
|
||||
|
||||
请严格按照以上游戏规则,开始游戏
|
||||
`
|
||||
@@ -26,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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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("开始游戏").
|
||||
|
||||
@@ -1 +1 @@
|
||||
v5.0.0-250704
|
||||
v5.0.0-250709
|
||||
|
||||
11
parser.go
11
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")
|
||||
}
|
||||
|
||||
149
report.go
149
report.go
@@ -1838,6 +1838,101 @@ const htmlTemplate = `<!DOCTYPE html>
|
||||
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 = `<!DOCTYPE html>
|
||||
{{if and $validator.msg (ne $validator.check_result "pass")}}
|
||||
<div class="validator-message">{{$validator.msg}}</div>
|
||||
{{end}}
|
||||
|
||||
<!-- AI Assertion Results -->
|
||||
{{if $validator.ai_result}}
|
||||
<div class="ai-assertion-section">
|
||||
<h5>🤖 AI Assertion Details</h5>
|
||||
|
||||
<!-- AI Assertion Screenshot -->
|
||||
{{if $validator.ai_result.image_path}}
|
||||
<div class="ai-screenshot-container">
|
||||
<span class="step-name">📸 AI Assertion Screenshot</span>
|
||||
{{if $validator.ai_result.screenshot_elapsed}}
|
||||
<span class="duration">{{formatDuration $validator.ai_result.screenshot_elapsed}}</span>
|
||||
{{end}}
|
||||
<div class="ai-screenshot">
|
||||
{{$base64Image := encodeImageBase64 $validator.ai_result.image_path}}
|
||||
{{if $base64Image}}
|
||||
<img src="data:image/jpeg;base64,{{$base64Image}}" alt="AI Assertion Screenshot" onclick="openImageModal(this.src)" />
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- AI Model Analysis -->
|
||||
<div class="ai-analysis-container">
|
||||
<span class="step-name">🧠 AI Model Analysis</span>
|
||||
{{if $validator.ai_result.model_call_elapsed}}
|
||||
<span class="duration">{{formatDuration $validator.ai_result.model_call_elapsed}}</span>
|
||||
{{end}}
|
||||
<div class="ai-analysis-content">
|
||||
{{if $validator.ai_result.assertion_result.model_name}}
|
||||
<div class="model-info">🤖 Model: {{$validator.ai_result.assertion_result.model_name}}</div>
|
||||
{{end}}
|
||||
{{if $validator.ai_result.assertion_result.usage}}
|
||||
<div class="usage-info">📊 Tokens: {{$validator.ai_result.assertion_result.usage.PromptTokens}} in / {{$validator.ai_result.assertion_result.usage.CompletionTokens}} out / {{$validator.ai_result.assertion_result.usage.TotalTokens}} total</div>
|
||||
{{end}}
|
||||
{{if $validator.ai_result.resolution}}
|
||||
<div class="model-info">📐 Resolution: {{$validator.ai_result.resolution.Width}}x{{$validator.ai_result.resolution.Height}}</div>
|
||||
{{end}}
|
||||
{{if $validator.ai_result.assertion_result.thought}}
|
||||
<div class="ai-thought">
|
||||
<strong>💭 AI Reasoning:</strong>
|
||||
<div class="thought-content">{{$validator.ai_result.assertion_result.thought}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if $validator.ai_result.assertion_result.content}}
|
||||
<div class="ai-raw-response">
|
||||
<strong>📝 Raw Model Response:</strong>
|
||||
<div class="response-content">{{$validator.ai_result.assertion_result.content}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
@@ -1042,7 +1042,8 @@ func validateUI(ud *uixt.XTDriver, iValidators []interface{}, parser *Parser, st
|
||||
}
|
||||
|
||||
// Perform validation
|
||||
err = ud.DoValidation(validator.Check, validator.Assert, expected, validator.Message)
|
||||
validationResult.AIResult, err = ud.DoValidation(
|
||||
validator.Check, validator.Assert, expected, validator.Message)
|
||||
if err != nil {
|
||||
// Add the failed validation result to the list before returning error
|
||||
validateResults = append(validateResults, validationResult)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/internal/config"
|
||||
"github.com/httprunner/httprunner/v5/internal/version"
|
||||
"github.com/httprunner/httprunner/v5/uixt"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
)
|
||||
|
||||
@@ -233,6 +234,7 @@ type Address struct {
|
||||
|
||||
type ValidationResult struct {
|
||||
Validator
|
||||
CheckValue interface{} `json:"check_value" yaml:"check_value"`
|
||||
CheckResult string `json:"check_result" yaml:"check_result"`
|
||||
CheckValue interface{} `json:"check_value" yaml:"check_value"`
|
||||
CheckResult string `json:"check_result" yaml:"check_result"`
|
||||
AIResult *uixt.AIExecutionResult `json:"ai_result,omitempty" yaml:"ai_result,omitempty"` // store AI assertion result for displaying in report
|
||||
}
|
||||
|
||||
@@ -33,8 +33,9 @@ type AssertOptions struct {
|
||||
type AssertionResult struct {
|
||||
Pass bool `json:"pass"`
|
||||
Thought string `json:"thought"`
|
||||
ModelName string `json:"model_name"` // model name used for assertion
|
||||
Usage *schema.TokenUsage `json:"usage,omitempty"` // token usage statistics
|
||||
Content string `json:"content,omitempty"` // raw response content
|
||||
ModelName string `json:"model_name"` // model name used for assertion
|
||||
Usage *schema.TokenUsage `json:"usage,omitempty"` // token usage statistics
|
||||
}
|
||||
|
||||
// Asserter handles assertion using different AI models
|
||||
@@ -180,5 +181,6 @@ func parseAssertionResult(content string, modelType option.LLMServiceType) (*Ass
|
||||
}
|
||||
|
||||
result.ModelName = string(modelType)
|
||||
result.Content = content // Store the original response content
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ func (dExt *XTDriver) StartToGoal(ctx context.Context, prompt string, opts ...op
|
||||
log.Error().Err(err).
|
||||
Str("action", toolCall.Function.Name).
|
||||
Msg("invoke tool call failed")
|
||||
subActionResult.Error = err
|
||||
subActionResult.Error = err.Error()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -371,7 +371,6 @@ func (dExt *XTDriver) executeAIAssert(assertion string, screenResult *ScreenResu
|
||||
|
||||
if !result.Pass {
|
||||
assertResult.Error = result.Thought
|
||||
return assertResult, errors.New(result.Thought)
|
||||
}
|
||||
|
||||
return assertResult, nil
|
||||
@@ -563,7 +562,7 @@ type SubActionResult struct {
|
||||
Arguments interface{} `json:"arguments,omitempty"` // arguments passed to the sub-action
|
||||
StartTime int64 `json:"start_time"` // sub-action start time
|
||||
Elapsed int64 `json:"elapsed_ms"` // sub-action elapsed time(ms)
|
||||
Error error `json:"error,omitempty"` // sub-action execution result
|
||||
Error string `json:"error,omitempty"` // sub-action execution result
|
||||
SessionData
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/internal/json"
|
||||
"github.com/httprunner/httprunner/v5/uixt/option"
|
||||
)
|
||||
|
||||
type Attachments map[string]interface{}
|
||||
@@ -151,32 +152,38 @@ func (s *DriverSession) buildURL(urlStr string) (string, error) {
|
||||
return baseURL.ResolveReference(relativeURL).String(), nil
|
||||
}
|
||||
|
||||
func (s *DriverSession) GET(urlStr string) (rawResp DriverRawResponse, err error) {
|
||||
return s.RequestWithRetry(http.MethodGet, urlStr, nil)
|
||||
func (s *DriverSession) GET(urlStr string, opts ...option.ActionOption) (rawResp DriverRawResponse, err error) {
|
||||
return s.RequestWithRetry(http.MethodGet, urlStr, nil, opts...)
|
||||
}
|
||||
|
||||
func (s *DriverSession) POST(data interface{}, urlStr string) (rawResp DriverRawResponse, err error) {
|
||||
func (s *DriverSession) POST(data interface{}, urlStr string, opts ...option.ActionOption) (rawResp DriverRawResponse, err error) {
|
||||
var bsJSON []byte = nil
|
||||
if data != nil {
|
||||
if bsJSON, err = json.Marshal(data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return s.RequestWithRetry(http.MethodPost, urlStr, bsJSON)
|
||||
return s.RequestWithRetry(http.MethodPost, urlStr, bsJSON, opts...)
|
||||
}
|
||||
|
||||
func (s *DriverSession) DELETE(urlStr string) (rawResp DriverRawResponse, err error) {
|
||||
return s.RequestWithRetry(http.MethodDelete, urlStr, nil)
|
||||
func (s *DriverSession) DELETE(urlStr string, opts ...option.ActionOption) (rawResp DriverRawResponse, err error) {
|
||||
return s.RequestWithRetry(http.MethodDelete, urlStr, nil, opts...)
|
||||
}
|
||||
|
||||
func (s *DriverSession) RequestWithRetry(method string, urlStr string, rawBody []byte) (
|
||||
func (s *DriverSession) RequestWithRetry(method string, urlStr string, rawBody []byte, opts ...option.ActionOption) (
|
||||
rawResp DriverRawResponse, err error,
|
||||
) {
|
||||
var lastError error
|
||||
|
||||
for attempt := 1; attempt <= s.maxRetry; attempt++ {
|
||||
maxRetry := s.maxRetry
|
||||
options := option.NewActionOptions(opts...)
|
||||
if options.MaxRetryTimes > 0 {
|
||||
maxRetry = options.MaxRetryTimes
|
||||
}
|
||||
|
||||
for attempt := 1; attempt <= maxRetry; attempt++ {
|
||||
// Execute the request
|
||||
rawResp, err = s.Request(method, urlStr, rawBody)
|
||||
rawResp, err = s.Request(method, urlStr, rawBody, opts...)
|
||||
if err == nil {
|
||||
if attempt > 1 {
|
||||
log.Info().Msgf("request succeeded after %d attempts", attempt)
|
||||
@@ -210,9 +217,15 @@ func (s *DriverSession) RequestWithRetry(method string, urlStr string, rawBody [
|
||||
return nil, lastError
|
||||
}
|
||||
|
||||
func (s *DriverSession) Request(method string, urlStr string, rawBody []byte) (
|
||||
func (s *DriverSession) Request(method string, urlStr string, rawBody []byte, opts ...option.ActionOption) (
|
||||
rawResp DriverRawResponse, err error,
|
||||
) {
|
||||
timeout := s.timeout
|
||||
options := option.NewActionOptions(opts...)
|
||||
if options.Timeout > 0 {
|
||||
timeout = time.Duration(options.Timeout) * time.Second
|
||||
}
|
||||
|
||||
// build final URL
|
||||
rawURL, err := s.buildURL(urlStr)
|
||||
if err != nil {
|
||||
@@ -252,7 +265,7 @@ func (s *DriverSession) Request(method string, urlStr string, rawBody []byte) (
|
||||
logger.Msg("request uixt driver")
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithTimeout(s.ctx, s.timeout)
|
||||
ctx, cancel := context.WithTimeout(s.ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, rawURL, bytes.NewBuffer(rawBody))
|
||||
|
||||
@@ -65,8 +65,8 @@ func convertToAbsolutePoint(driver IDriver, x, y float64) (absX, absY float64, e
|
||||
}
|
||||
|
||||
func convertToAbsoluteCoordinates(driver IDriver, fromX, fromY, toX, toY float64) (
|
||||
absFromX, absFromY, absToX, absToY float64, err error) {
|
||||
|
||||
absFromX, absFromY, absToX, absToY float64, err error,
|
||||
) {
|
||||
// absolute coordinates
|
||||
if fromX > 1 || toX > 1 || fromY > 1 || toY > 1 {
|
||||
return fromX, fromY, toX, toY, nil
|
||||
@@ -190,32 +190,43 @@ func (dExt *XTDriver) assertSelector(selector, assert string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dExt *XTDriver) DoValidation(check, assert, expected string, message ...string) (err error) {
|
||||
func (dExt *XTDriver) DoValidation(check, assert, expected string, message ...string) (aiResult *AIExecutionResult, err error) {
|
||||
switch check {
|
||||
case option.SelectorOCR:
|
||||
err = dExt.assertOCR(expected, assert)
|
||||
case option.SelectorAI:
|
||||
_, err = dExt.AIAssert(expected)
|
||||
aiResult, err = dExt.AIAssert(expected)
|
||||
case option.SelectorForegroundApp:
|
||||
err = dExt.assertForegroundApp(expected, assert)
|
||||
case option.SelectorSelector:
|
||||
err = dExt.assertSelector(expected, assert)
|
||||
default:
|
||||
return fmt.Errorf("validator %s not implemented", check)
|
||||
return nil, fmt.Errorf("validator %s not implemented", check)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// Technical error (not assertion failure)
|
||||
if message == nil {
|
||||
message = []string{""}
|
||||
}
|
||||
log.Error().Err(err).Str("assert", assert).Str("expect", expected).
|
||||
Str("msg", message[0]).Msg("validate failed")
|
||||
return err
|
||||
return nil, err
|
||||
} else if aiResult != nil {
|
||||
// Check assertion result instead of relying on error
|
||||
if !aiResult.AssertionResult.Pass {
|
||||
return aiResult, errors.New(aiResult.AssertionResult.Thought)
|
||||
}
|
||||
log.Info().Str("check", check).Str("assert", assert).
|
||||
Str("expect", expected).
|
||||
Interface("ai_assertion_result", aiResult.AssertionResult).
|
||||
Msg("ai assertion passed")
|
||||
return aiResult, nil
|
||||
} else {
|
||||
log.Info().Str("check", check).Str("assert", assert).
|
||||
Str("expect", expected).Msg("validate success")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
log.Info().Str("check", check).Str("assert", assert).
|
||||
Str("expect", expected).Msg("validate success")
|
||||
return nil
|
||||
}
|
||||
|
||||
type SleepConfig struct {
|
||||
|
||||
@@ -160,22 +160,22 @@ func (dev *IOSDevice) Setup() error {
|
||||
if version.GreaterThan(semver.MustParse("17.4.0")) {
|
||||
info, err := tunnel.TunnelInfoForDevice(dev.DeviceEntry.Properties.SerialNumber, ios.HttpApiHost(), ios.HttpApiPort())
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(code.DeviceConnectionError, err.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
|
||||
return errors.Wrap(code.DeviceConnectionError, err.Error())
|
||||
}
|
||||
defer rsdService.Close()
|
||||
rsdProvider, err := rsdService.Handshake()
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(code.DeviceConnectionError, err.Error())
|
||||
}
|
||||
device, err := ios.GetDeviceWithAddress(dev.DeviceEntry.Properties.SerialNumber, info.Address, rsdProvider)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(code.DeviceConnectionError, err.Error())
|
||||
}
|
||||
device.UserspaceTUN = dev.DeviceEntry.UserspaceTUN
|
||||
device.UserspaceTUNPort = dev.DeviceEntry.UserspaceTUNPort
|
||||
@@ -470,7 +470,7 @@ func (dev *IOSDevice) getVersion() (version *semver.Version, err error) {
|
||||
version, err = ios.GetProductVersion(dev.DeviceEntry)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to get version")
|
||||
return nil, err
|
||||
return nil, errors.Wrap(code.DeviceGetInfoError, err.Error())
|
||||
}
|
||||
log.Info().Str("version", version.String()).Msg("get ios device version")
|
||||
return version, nil
|
||||
|
||||
@@ -75,9 +75,9 @@ func (t *ToolSleep) Implement() server.ToolHandlerFunc {
|
||||
case <-time.After(duration):
|
||||
// Normal completion
|
||||
case <-ctx.Done():
|
||||
// Interrupted by context cancellation (e.g., CTRL+C)
|
||||
log.Warn().Msg("sleep interrupted by cancellation")
|
||||
return nil, fmt.Errorf("sleep interrupted: %w", ctx.Err())
|
||||
// Interrupted by context cancellation (interrupt signal, timeout, time limit)
|
||||
log.Info().Msg("sleep interrupted by context cancellation")
|
||||
// Don't return error - let the upper layer handle timeout/time limit logic
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("Successfully slept for %v seconds", actualSeconds)
|
||||
@@ -157,9 +157,9 @@ func (t *ToolSleepMS) Implement() server.ToolHandlerFunc {
|
||||
case <-time.After(duration):
|
||||
// Normal completion
|
||||
case <-ctx.Done():
|
||||
// Interrupted by context cancellation (e.g., CTRL+C)
|
||||
log.Warn().Msg("sleep interrupted by cancellation")
|
||||
return nil, fmt.Errorf("sleep interrupted: %w", ctx.Err())
|
||||
// Interrupted by context cancellation (interrupt signal, timeout, time limit)
|
||||
log.Info().Msg("sleep interrupted by context cancellation")
|
||||
// Don't return error - let the upper layer handle timeout/time limit logic
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("Successfully slept for %d milliseconds", actualMilliseconds)
|
||||
|
||||
Reference in New Issue
Block a user