Files
httprunner/tests/step_ui_test.go
lilong.129 f6e7e970f8 feat: 实现 AIQuery 功能并支持 OutputSchema
- 新增 AIQuery 方法到 StepMobile,支持使用自然语言从屏幕中提取信息
- 实现 AIQuery 在 driver_ext_ai.go 中的完整功能,包括屏幕截图和 LLM 查询
- 添加 OutputSchema 支持,允许用户定义自定义输出格式进行结构化查询
- 新增 ToolAIQuery MCP 工具,完整集成到 MCP 服务器中
- 在 ActionOptions 中添加 OutputSchema 字段和 WithOutputSchema 选项函数
- 添加 ACTION_Query 的配置支持和字段映射
- 完善测试覆盖:
  * 添加 TestAIQuery 单元测试,包含多种 OutputSchema 使用场景
  * 添加 TestToolAIQuery MCP 工具测试
  * 定义 GameInfo、UIElementInfo 等结构体用于测试
- 更新文档:
  * 在 docs/uixt/ai.md 中添加完整的 AIQuery 使用指南
  * 包含基本用法、OutputSchema 示例、最佳实践等
- 支持复杂的嵌套结构体和数组类型的 OutputSchema
- 与现有 AIAction、AIAssert 功能保持一致的 API 设计
2025-06-13 10:27:08 +08:00

294 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//go:build localtest
package tests
import (
"testing"
hrp "github.com/httprunner/httprunner/v5"
"github.com/httprunner/httprunner/v5/uixt/option"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// GameInfo 定义游戏界面分析的输出格式
type GameInfo struct {
Content string `json:"content"` // 必须:人类可读描述
Thought string `json:"thought"` // 必须AI推理过程
GameType string `json:"game_type"` // 游戏类型
Rows int `json:"rows"` // 行数
Cols int `json:"cols"` // 列数
Icons []string `json:"icons"` // 图标类型
TotalIcons int `json:"total_icons"` // 图标总数
}
// UIElementInfo 定义UI元素分析的输出格式
type UIElementInfo struct {
Content string `json:"content"` // 必须:人类可读描述
Thought string `json:"thought"` // 必须AI推理过程
ScreenType string `json:"screen_type"` // 屏幕类型
Elements []UIElement `json:"elements"` // UI元素列表
ButtonCount int `json:"button_count"` // 按钮数量
TextCount int `json:"text_count"` // 文本数量
}
// UIElement 定义单个UI元素
type UIElement struct {
Type string `json:"type"` // 元素类型 (button, text, input等)
Text string `json:"text"` // 元素文本
Clickable bool `json:"clickable"` // 是否可点击
Description string `json:"description"` // 元素描述
}
func TestIOSSettingsAction(t *testing.T) {
testCase := &hrp.TestCase{
Config: hrp.NewConfig("ios ui action on Settings").
SetIOS(option.WithWDAPort(8700), option.WithWDAMjpegPort(8800)),
TestSteps: []hrp.IStep{
hrp.NewStep("launch Settings").
IOS().Home().TapByOCR("设置").
Validate().
AssertNameExists("飞行模式").
AssertLabelExists("蓝牙").
AssertOCRExists("个人热点"),
hrp.NewStep("swipe up and down").
IOS().SwipeUp().SwipeUp().SwipeDown(),
},
}
err := hrp.NewRunner(t).Run(testCase)
assert.Nil(t, err)
}
func TestIOSSearchApp(t *testing.T) {
testCase := &hrp.TestCase{
Config: hrp.NewConfig("ios ui action on Search App 资源库"),
TestSteps: []hrp.IStep{
hrp.NewStep("进入 App 资源库 搜索框").
IOS().Home().SwipeLeft().SwipeLeft().TapByCV("dewey-search-field").
Validate().
AssertLabelExists("取消"),
hrp.NewStep("搜索抖音").
IOS().Input("抖音\n"),
},
}
err := hrp.NewRunner(t).Run(testCase)
assert.Nil(t, err)
}
func TestIOSAppLaunch(t *testing.T) {
testCase := &hrp.TestCase{
Config: hrp.NewConfig("启动 & 关闭 App").
SetIOS(option.WithWDAPort(8700), option.WithWDAMjpegPort(8800)),
TestSteps: []hrp.IStep{
hrp.NewStep("终止今日头条").
IOS().AppTerminate("com.ss.iphone.article.News"),
hrp.NewStep("启动今日头条").
IOS().AppLaunch("com.ss.iphone.article.News"),
hrp.NewStep("终止今日头条").
IOS().AppTerminate("com.ss.iphone.article.News"),
hrp.NewStep("启动今日头条").
IOS().AppLaunch("com.ss.iphone.article.News"),
},
}
err := hrp.NewRunner(t).Run(testCase)
assert.Nil(t, err)
}
func TestAndroidAction(t *testing.T) {
testCase := &hrp.TestCase{
Config: hrp.NewConfig("android ui action"),
TestSteps: []hrp.IStep{
hrp.NewStep("launch douyin").
Android().Serial("xxx").TapByOCR("抖音").
Validate().
AssertNameExists("首页", "首页 tab 不存在").
AssertNameExists("消息", "消息 tab 不存在"),
hrp.NewStep("swipe up and down").
Android().Serial("xxx").SwipeUp().SwipeUp().SwipeDown(),
},
}
err := hrp.NewRunner(t).Run(testCase)
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").
SetLLMService(option.DOUBAO_1_5_THINKING_VISION_PRO_250428),
TestSteps: []hrp.IStep{
hrp.NewStep("launch settings").
Android().AIAction("进入手机系统设置").
Validate().
AssertAI("当前位于手机设置页面"),
hrp.NewStep("turn on fly mode").
Android().AIAction("开启飞行模式").
Validate().
AssertAI("飞行模式已开启"),
},
}
err := hrp.NewRunner(t).Run(testCase)
assert.Nil(t, err)
}
func TestAIQuery(t *testing.T) {
testCase := &hrp.TestCase{
Config: hrp.NewConfig("AIQuery Demo with OutputSchema").
SetLLMService(option.DOUBAO_SEED_1_6_250615), // Configure LLM service for AI operations
TestSteps: []hrp.IStep{
// Step 1: Take a screenshot for analysis
hrp.NewStep("Take Screenshot").
Android().
ScreenShot(),
// Step 2: Basic AIQuery without OutputSchema
hrp.NewStep("Basic Query").
Android().
AIQuery("Please describe what is displayed on the screen and identify any interactive elements"),
// Step 3: Use AIQuery to extract specific information
hrp.NewStep("Extract App Information").
Android().
AIQuery("What apps are visible on the screen? List them as a comma-separated string"),
// Step 4: Use AIQuery for UI element analysis
hrp.NewStep("Analyze UI Elements").
Android().
AIQuery("Are there any buttons or clickable elements visible? Describe their locations and purposes"),
// Step 5: Use AIQuery with validation
hrp.NewStep("Query and Validate").
Android().
AIQuery("Is the home screen currently displayed?").
Validate().
AssertAI("The query result should indicate whether home screen is visible"),
// Step 6: Use AIQuery with simple custom OutputSchema
hrp.NewStep("Query with Simple Custom Schema").
Android().
AIQuery("Analyze the screen and provide structured information about UI elements",
option.WithOutputSchema(struct {
Content string `json:"content"`
Thought string `json:"thought"`
ElementType string `json:"element_type"`
ElementText []string `json:"element_text"`
ButtonCount int `json:"button_count"`
}{})),
// Step 7: Use AIQuery with GameInfo OutputSchema
hrp.NewStep("Game Analysis with Custom Schema").
Android().
AIQuery("分析这个游戏界面,告诉我游戏类型、行列数和图标信息",
option.WithOutputSchema(GameInfo{})),
// Step 8: Use AIQuery with UIElementInfo OutputSchema
hrp.NewStep("UI Element Analysis with Custom Schema").
Android().
AIQuery("分析屏幕上的UI元素识别所有按钮、文本和可交互元素",
option.WithOutputSchema(UIElementInfo{})),
// Step 9: Complex analysis with nested structure
hrp.NewStep("Complex Analysis with Nested Schema").
Android().
AIQuery("Provide a comprehensive analysis of this interface including all interactive elements and their properties",
option.WithOutputSchema(struct {
Content string `json:"content"`
Thought string `json:"thought"`
AppName string `json:"app_name"`
ScreenTitle string `json:"screen_title"`
MainActions []struct {
Name string `json:"name"`
Description string `json:"description"`
Available bool `json:"available"`
} `json:"main_actions"`
NavigationElements []struct {
Type string `json:"type"`
Label string `json:"label"`
Position string `json:"position"`
} `json:"navigation_elements"`
ContentSummary struct {
HasImages bool `json:"has_images"`
HasText bool `json:"has_text"`
HasForms bool `json:"has_forms"`
Keywords []string `json:"keywords"`
} `json:"content_summary"`
}{})),
},
}
err := hrp.NewRunner(t).Run(testCase)
assert.Nil(t, err)
}