diff --git a/examples/game/romantic_restaurant/game_romantic_restaurant.json b/examples/game/romantic_restaurant/game_romantic_restaurant.json new file mode 100644 index 00000000..b0aea6ce --- /dev/null +++ b/examples/game/romantic_restaurant/game_romantic_restaurant.json @@ -0,0 +1,88 @@ +{ + "config": { + "name": "浪漫餐厅小游戏自动化测试", + "variables": { + "package_name": "com.ss.android.ugc.aweme" + }, + "ai_options": { + "llm_service": "doubao-1.5-thinking-vision-pro-250428" + } + }, + "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": "start_to_goal", + "params": "搜索「浪漫餐厅」,进入小游戏", + "options": { + "pre_mark_operation": true + } + } + ] + }, + "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、将棋子拖拽至相同棋子,可升级生成新棋子\n3、拖拽相同棋子时,被部分遮挡的棋子只能作为拖拽终点,不能作为拖拽起点\n4、当游戏界面中没有相同棋子时,可以点击游戏页面中央的购物袋生成新的棋子\n5、若不知道如何操作,请按照游戏指引进行游玩\n\n请严格按照以上游戏规则,开始游戏\n", + "options": { + "timeout": 300, + "pre_mark_operation": true + } + } + ] + } + }, + { + "name": "退出抖音 app", + "android": { + "os_type": "android", + "actions": [ + { + "method": "app_terminate", + "params": "$package_name" + } + ] + } + } + ] +} diff --git a/examples/game/romantic_restaurant/main_test.go b/examples/game/romantic_restaurant/main_test.go new file mode 100644 index 00000000..14af94a7 --- /dev/null +++ b/examples/game/romantic_restaurant/main_test.go @@ -0,0 +1,56 @@ +package game_romantic_restaurant + +import ( + "testing" + + "github.com/stretchr/testify/require" + + hrp "github.com/httprunner/httprunner/v5" + "github.com/httprunner/httprunner/v5/uixt/option" +) + +func TestGameRomanticRestaurant(t *testing.T) { + userInstruction := `浪漫餐厅是一款经营类游戏,以下是游戏的基本规则说明: +1、点击右下角锅铲,开始任务 +2、将棋子拖拽至相同棋子,可升级生成新棋子 +3、拖拽相同棋子时,被部分遮挡的棋子只能作为拖拽终点,不能作为拖拽起点 +4、当游戏界面中没有相同棋子时,可以点击游戏页面中央的购物袋生成新的棋子 +5、若不知道如何操作,请按照游戏指引进行游玩 + +请严格按照以上游戏规则,开始游戏 +` + testCase := &hrp.TestCase{ + Config: hrp.NewConfig("浪漫餐厅小游戏自动化测试"). + SetLLMService(option.DOUBAO_1_5_THINKING_VISION_PRO_250428). + WithVariables(map[string]interface{}{ + "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("进入「浪漫餐厅」小游戏"). + Android(). + StartToGoal("搜索「浪漫餐厅」,进入小游戏", + option.WithPreMarkOperation(true)). + Validate(). + AssertAI("当前位于游戏界面"), + hrp.NewStep("开始游戏"). + Android(). + StartToGoal(userInstruction, + option.WithPreMarkOperation(true), + option.WithTimeout(300)), // 5 minutes + hrp.NewStep("退出抖音 app"). + Android(). + AppTerminate("$package_name"), + }, + } + err := testCase.Dump2JSON("game_romantic_restaurant.json") + require.Nil(t, err) + + // err = hrp.NewRunner(t).Run(testCase) + // assert.Nil(t, err) +} diff --git a/examples/game/sudoku/game_sudoku.json b/examples/game/sudoku/game_sudoku.json new file mode 100644 index 00000000..063a8ccc --- /dev/null +++ b/examples/game/sudoku/game_sudoku.json @@ -0,0 +1,88 @@ +{ + "config": { + "name": "每天数独小游戏自动化测试", + "variables": { + "package_name": "com.ss.android.ugc.aweme" + }, + "ai_options": { + "llm_service": "doubao-1.5-thinking-vision-pro-250428" + } + }, + "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": "start_to_goal", + "params": "搜索「每天数独」,进入小游戏", + "options": { + "pre_mark_operation": true + } + } + ] + }, + "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、初始状态为白色方块,选择正确后变为黄色方块,选择错误后变为红底的 X。\n3、如果同一行或列有两个数字,则至少需要一个白底 X 分割它们作为间隔。\n4、如果数字与格子最大数相同时,该列或行必然全都是黄色方块。\n5、只能点击白色方块,不要重复点击同一个方块。\n\n请严格按照以上游戏规则,开始游戏\n", + "options": { + "timeout": 300, + "pre_mark_operation": true + } + } + ] + } + }, + { + "name": "退出抖音 app", + "android": { + "os_type": "android", + "actions": [ + { + "method": "app_terminate", + "params": "$package_name" + } + ] + } + } + ] +} diff --git a/examples/game/sudoku/main_test.go b/examples/game/sudoku/main_test.go index 348e29a7..dd509b9b 100644 --- a/examples/game/sudoku/main_test.go +++ b/examples/game/sudoku/main_test.go @@ -1 +1,56 @@ package game_sudoku + +import ( + "testing" + + "github.com/stretchr/testify/require" + + hrp "github.com/httprunner/httprunner/v5" + "github.com/httprunner/httprunner/v5/uixt/option" +) + +func TestGameSudoku(t *testing.T) { + userInstruction := `每天数独是一款逻辑推理游戏,玩家需要通过推理来确定黄色方块的所在位置,以下是游戏的基本规则说明: +1、方块外面的数字代表所在那一行或一列的黄色方块数量。 +2、初始状态为白色方块,选择正确后变为黄色方块,选择错误后变为红底的 X。 +3、如果同一行或列有两个数字,则至少需要一个白底 X 分割它们作为间隔。 +4、如果数字与格子最大数相同时,该列或行必然全都是黄色方块。 +5、只能点击白色方块,不要重复点击同一个方块。 + +请严格按照以上游戏规则,开始游戏 +` + testCase := &hrp.TestCase{ + Config: hrp.NewConfig("每天数独小游戏自动化测试"). + SetLLMService(option.DOUBAO_1_5_THINKING_VISION_PRO_250428). + WithVariables(map[string]interface{}{ + "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("进入「每天数独」小游戏"). + Android(). + StartToGoal("搜索「每天数独」,进入小游戏", + option.WithPreMarkOperation(true)). + Validate(). + AssertAI("当前页面底部包含「开始」按钮"), + hrp.NewStep("开始游戏"). + Android(). + StartToGoal(userInstruction, + option.WithPreMarkOperation(true), + option.WithTimeout(300)), // 5 minutes + hrp.NewStep("退出抖音 app"). + Android(). + AppTerminate("$package_name"), + }, + } + err := testCase.Dump2JSON("game_sudoku.json") + require.Nil(t, err) + + // err = hrp.NewRunner(t).Run(testCase) + // assert.Nil(t, err) +} diff --git a/examples/game/yanglegeyang/game_yanglegeyang.json b/examples/game/yanglegeyang/game_yanglegeyang.json index 9c72a02a..8ce49876 100644 --- a/examples/game/yanglegeyang/game_yanglegeyang.json +++ b/examples/game/yanglegeyang/game_yanglegeyang.json @@ -1,6 +1,9 @@ { "config": { "name": "羊了个羊小游戏自动化测试", + "variables": { + "package_name": "com.ss.android.ugc.aweme" + }, "ai_options": { "llm_service": "doubao-1.5-thinking-vision-pro-250428" } @@ -13,7 +16,7 @@ "actions": [ { "method": "app_launch", - "params": "com.ss.android.ugc.aweme" + "params": "$package_name" }, { "method": "sleep", @@ -25,8 +28,8 @@ { "check": "ui_foreground_app", "assert": "equal", - "expect": "com.ss.android.ugc.aweme", - "msg": "app [com.ss.android.ugc.aweme] should be in foreground" + "expect": "$package_name", + "msg": "app [$package_name] should be in foreground" } ] }, @@ -68,6 +71,18 @@ } ] } + }, + { + "name": "退出抖音 app", + "android": { + "os_type": "android", + "actions": [ + { + "method": "app_terminate", + "params": "$package_name" + } + ] + } } ] } diff --git a/examples/game/yanglegeyang/main_test.go b/examples/game/yanglegeyang/main_test.go index a6372347..691c749c 100644 --- a/examples/game/yanglegeyang/main_test.go +++ b/examples/game/yanglegeyang/main_test.go @@ -28,14 +28,17 @@ func TestGameYanglegeyang(t *testing.T) { testCase := &hrp.TestCase{ Config: hrp.NewConfig("羊了个羊小游戏自动化测试"). - SetLLMService(option.DOUBAO_1_5_THINKING_VISION_PRO_250428), + SetLLMService(option.DOUBAO_1_5_THINKING_VISION_PRO_250428). + WithVariables(map[string]interface{}{ + "package_name": "com.ss.android.ugc.aweme", + }), TestSteps: []hrp.IStep{ hrp.NewStep("启动抖音 app"). Android(). - AppLaunch("com.ss.android.ugc.aweme"). + AppLaunch("$package_name"). Sleep(5). Validate(). - AssertAppInForeground("com.ss.android.ugc.aweme"), + AssertAppInForeground("$package_name"), hrp.NewStep("进入「羊了个羊」小游戏"). Android(). StartToGoal("搜索「羊了个羊星球」,进入小程序,加入羊群进入游戏", @@ -47,6 +50,9 @@ func TestGameYanglegeyang(t *testing.T) { StartToGoal(userInstruction, option.WithPreMarkOperation(true), option.WithTimeout(300)), // 5 minutes + hrp.NewStep("退出抖音 app"). + Android(). + AppTerminate("$package_name"), }, } err := testCase.Dump2JSON("game_yanglegeyang.json") diff --git a/examples/game/zhuadae/game_zhuadae.json b/examples/game/zhuadae/game_zhuadae.json new file mode 100644 index 00000000..5fe446dd --- /dev/null +++ b/examples/game/zhuadae/game_zhuadae.json @@ -0,0 +1,111 @@ +{ + "config": { + "name": "抓大鹅小游戏自动化测试", + "variables": { + "package_name": "com.ss.android.ugc.aweme" + }, + "ai_options": { + "llm_service": "doubao-1.5-thinking-vision-pro-250428" + } + }, + "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": "start_to_goal", + "params": "搜索「抓大鹅」,启动小游戏", + "options": { + "pre_mark_operation": true + } + } + ] + }, + "validate": [ + { + "check": "ui_ai", + "assert": "ai_assert", + "expect": "当前页面底部包含「抓大鹅」按钮", + "msg": "assert ai prompt [当前页面底部包含「抓大鹅」按钮] failed" + } + ] + }, + { + "name": "进入「抓大鹅」小游戏", + "android": { + "os_type": "android", + "actions": [ + { + "method": "start_to_goal", + "params": "点击「抓大鹅」,进入小游戏", + "options": { + "pre_mark_operation": true + } + } + ] + }, + "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- 需要尽量避免抓取槽满的情况,抓取槽满时游戏失败。\n- 游戏通关后继续进入下一关,游戏失败后重新开始游戏。\n\n请严格按照以上游戏规则,开始游戏\n", + "options": { + "timeout": 300, + "pre_mark_operation": true + } + } + ] + } + }, + { + "name": "退出抖音 app", + "android": { + "os_type": "android", + "actions": [ + { + "method": "app_terminate", + "params": "$package_name" + } + ] + } + } + ] +} diff --git a/examples/game/zhuadae/main_test.go b/examples/game/zhuadae/main_test.go new file mode 100644 index 00000000..239a00e4 --- /dev/null +++ b/examples/game/zhuadae/main_test.go @@ -0,0 +1,64 @@ +package game_zhuadae + +import ( + "testing" + + "github.com/stretchr/testify/require" + + hrp "github.com/httprunner/httprunner/v5" + "github.com/httprunner/httprunner/v5/uixt/option" +) + +func TestGameZhuadaE(t *testing.T) { + userInstruction := `抓大鹅是一款抓取类小游戏,以下是游戏的基本规则说明: +1. 游戏目标: 玩家需要通过抓取图案来完成关卡,最终目标是清空所有图案。 +2. 抓取规则: +- 游戏界面中会出现多个图案,图案存在多层堆叠的情况,玩家需要点击图案将其抓取放入到槽中。 +- 当抓取到三个相同的图案放入抓取槽时,这三个图案会成功消除。 +- 需要尽量避免抓取槽满的情况,抓取槽满时游戏失败。 +- 游戏通关后继续进入下一关,游戏失败后重新开始游戏。 + +请严格按照以上游戏规则,开始游戏 +` + + testCase := &hrp.TestCase{ + Config: hrp.NewConfig("抓大鹅小游戏自动化测试"). + SetLLMService(option.DOUBAO_1_5_THINKING_VISION_PRO_250428). + WithVariables(map[string]interface{}{ + "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("启动「抓大鹅」小游戏"). + Android(). + StartToGoal("搜索「抓大鹅」,启动小游戏", + option.WithPreMarkOperation(true)). + Validate(). + AssertAI("当前页面底部包含「抓大鹅」按钮"), + hrp.NewStep("进入「抓大鹅」小游戏"). + Android(). + StartToGoal("点击「抓大鹅」,进入小游戏", + option.WithPreMarkOperation(true)). + Validate(). + AssertAI("当前页面底部包含「移出」「凑齐」「打乱」按钮"), + hrp.NewStep("开始游戏"). + Android(). + StartToGoal(userInstruction, + option.WithPreMarkOperation(true), + option.WithTimeout(300)), // 5 minutes + hrp.NewStep("退出抖音 app"). + Android(). + AppTerminate("$package_name"), + }, + } + err := testCase.Dump2JSON("game_zhuadae.json") + require.Nil(t, err) + + // err = hrp.NewRunner(t).Run(testCase) + // assert.Nil(t, err) +} diff --git a/internal/version/VERSION b/internal/version/VERSION index 524b9a16..9066599a 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-250630 +v5.0.0-250701 diff --git a/runner.go b/runner.go index 81bb88fe..083218d5 100644 --- a/runner.go +++ b/runner.go @@ -36,9 +36,7 @@ import ( // Run starts to run testcase with default configs. func Run(t *testing.T, testcases ...ITestCase) error { - err := NewRunner(t).SetSaveTests(true).Run(testcases...) - code.GetErrorCode(err) - return err + return NewRunner(t).SetSaveTests(true).Run(testcases...) } // NewRunner constructs a new runner instance. @@ -234,7 +232,9 @@ func (r *HRPRunner) Run(testcases ...ITestCase) (err error) { // this ensures they run regardless of how the function exits defer func() { s.Time.Duration = time.Since(s.Time.StartAt).Seconds() - log.Info().Int("duration(s)", int(s.Time.Duration)).Msg("run testcase finished") + exitCode := code.GetErrorCode(err) + log.Info().Int("duration(s)", int(s.Time.Duration)). + Int("exitCode", exitCode).Msg("run testcase finished") // save summary if r.saveTests { diff --git a/runner_uixt.go b/runner_uixt.go index 36e6078a..61968309 100644 --- a/runner_uixt.go +++ b/runner_uixt.go @@ -14,13 +14,14 @@ import ( "syscall" "time" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/json" "github.com/httprunner/httprunner/v5/internal/version" "github.com/httprunner/httprunner/v5/uixt" "github.com/httprunner/httprunner/v5/uixt/option" - "github.com/pkg/errors" - "github.com/rs/zerolog/log" ) type UIXTRunner struct { @@ -219,7 +220,7 @@ func (configs *UIXTConfig) getWDALocalPort(udid string) (string, error) { "device_id": udid, }) req, err := http.NewRequest("POST", - fmt.Sprintf("http://127.0.0.1:%d/get_device_port", configs.WDAMjpegPort), + fmt.Sprintf("http://localhost:%d/get_device_port", configs.WDAMjpegPort), bytes.NewBuffer(payloadBytes)) if err != nil { return "", errors.Wrap(err, "create request failed") diff --git a/uixt/browser_driver.go b/uixt/browser_driver.go index 71325483..118fc83f 100644 --- a/uixt/browser_driver.go +++ b/uixt/browser_driver.go @@ -19,7 +19,7 @@ import ( "github.com/httprunner/httprunner/v5/uixt/types" ) -const BROWSER_LOCAL_ADDRESS = "localhost:8093" +const BROWSER_BASE_URL = "http://localhost:8093" type WebAgentResponse struct { Code int `json:"code"` @@ -35,9 +35,8 @@ type CreateBrowserResponse struct { } type BrowserDriver struct { - urlPrefix *url.URL - Session *DriverSession - Device *BrowserDevice + Session *DriverSession + Device *BrowserDevice } type BrowserInfo struct { @@ -61,7 +60,7 @@ func CreateBrowser(timeout int, width, height int) (browserInfo *BrowserInfo, er return nil, err } - rawURL := "http://" + BROWSER_LOCAL_ADDRESS + "/api/v1/create_browser" + rawURL := BROWSER_BASE_URL + "/api/v1/create_browser" req, err := http.NewRequest(http.MethodPost, rawURL, bytes.NewBuffer(bsJSON)) req.Header.Set("Content-Type", "application/json") @@ -98,20 +97,23 @@ func NewBrowserDriver(device *BrowserDevice) (driver *BrowserDriver, err error) log.Info().Msg("init NewBrowserDriver driver") driver = new(BrowserDriver) driver.Device = device - driver.urlPrefix = &url.URL{} - driver.urlPrefix.Host = BROWSER_LOCAL_ADDRESS - driver.urlPrefix.Scheme = "http" - driver.Session = NewDriverSession() - driver.Session.ID = device.UUID() + + err = driver.Setup() + if err != nil { + return nil, errors.Wrap(err, "setup browser driver failed") + } + return driver, nil } func (wd *BrowserDriver) Setup() error { + wd.Session = NewDriverSession() err := wd.Session.SetupPortForward(8093) if err != nil { return err } - wd.Session.SetBaseURL(BROWSER_LOCAL_ADDRESS) + wd.Session.SetBaseURL(BROWSER_BASE_URL) + wd.Session.ID = wd.Device.UUID() return nil } @@ -263,7 +265,7 @@ func (wd *BrowserDriver) SecondaryClickBySelector(selector string, options ...op func (wd *BrowserDriver) GetElementTextBySelector(selector string, options ...option.ActionOption) (text string, err error) { actionOptions := option.NewActionOptions(options...) - baseURL := fmt.Sprintf("http://%s/api/v1/%s/element_text", BROWSER_LOCAL_ADDRESS, wd.Session.ID) + baseURL := fmt.Sprintf("%s/api/v1/%s/element_text", BROWSER_BASE_URL, wd.Session.ID) // 使用 url.Values 构建查询参数 params := url.Values{} @@ -404,10 +406,7 @@ func (wd *BrowserDriver) ScreenShot(options ...option.ActionOption) (*bytes.Buff } func (wd *BrowserDriver) concatURL(elem ...string) string { - tmp, _ := url.Parse(wd.urlPrefix.String()) - commonPath := path.Join(append([]string{wd.urlPrefix.Path}, "api/v1/")...) - tmp.Path = path.Join(append([]string{commonPath}, elem...)...) - return tmp.String() + return path.Join(append([]string{"/api/v1"}, elem...)...) } func (wd *BrowserDriver) Status() (deviceStatus types.DeviceStatus, err error) {