diff --git a/cmd/run.go b/cmd/run.go index fe9721b6..5d566f78 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -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 } diff --git a/docs/cmd/hrp.md b/docs/cmd/hrp.md index 9b57d269..944e56ab 100644 --- a/docs/cmd/hrp.md +++ b/docs/cmd/hrp.md @@ -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 diff --git a/docs/cmd/hrp_adb.md b/docs/cmd/hrp_adb.md index e4f26177..f7b1b740 100644 --- a/docs/cmd/hrp_adb.md +++ b/docs/cmd/hrp_adb.md @@ -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 diff --git a/docs/cmd/hrp_adb_devices.md b/docs/cmd/hrp_adb_devices.md index ba1296c3..015e76e9 100644 --- a/docs/cmd/hrp_adb_devices.md +++ b/docs/cmd/hrp_adb_devices.md @@ -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 diff --git a/docs/cmd/hrp_adb_install.md b/docs/cmd/hrp_adb_install.md index 56687299..d47962af 100644 --- a/docs/cmd/hrp_adb_install.md +++ b/docs/cmd/hrp_adb_install.md @@ -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 diff --git a/docs/cmd/hrp_adb_screencap.md b/docs/cmd/hrp_adb_screencap.md index 5c656ea5..8e950a6b 100644 --- a/docs/cmd/hrp_adb_screencap.md +++ b/docs/cmd/hrp_adb_screencap.md @@ -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 diff --git a/docs/cmd/hrp_build.md b/docs/cmd/hrp_build.md index eab54dd9..dc5ebdb2 100644 --- a/docs/cmd/hrp_build.md +++ b/docs/cmd/hrp_build.md @@ -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 diff --git a/docs/cmd/hrp_convert.md b/docs/cmd/hrp_convert.md index 81e985ea..d8c37ed1 100644 --- a/docs/cmd/hrp_convert.md +++ b/docs/cmd/hrp_convert.md @@ -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 diff --git a/docs/cmd/hrp_ios.md b/docs/cmd/hrp_ios.md index f38307ed..63500f19 100644 --- a/docs/cmd/hrp_ios.md +++ b/docs/cmd/hrp_ios.md @@ -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 diff --git a/docs/cmd/hrp_ios_apps.md b/docs/cmd/hrp_ios_apps.md index 2529c98f..1e42aec4 100644 --- a/docs/cmd/hrp_ios_apps.md +++ b/docs/cmd/hrp_ios_apps.md @@ -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 diff --git a/docs/cmd/hrp_ios_devices.md b/docs/cmd/hrp_ios_devices.md index 6b4e15c6..13e29866 100644 --- a/docs/cmd/hrp_ios_devices.md +++ b/docs/cmd/hrp_ios_devices.md @@ -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 diff --git a/docs/cmd/hrp_ios_install.md b/docs/cmd/hrp_ios_install.md index 5b85c94e..c3985c73 100644 --- a/docs/cmd/hrp_ios_install.md +++ b/docs/cmd/hrp_ios_install.md @@ -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 diff --git a/docs/cmd/hrp_ios_mount.md b/docs/cmd/hrp_ios_mount.md index 43312b16..26efd7d9 100644 --- a/docs/cmd/hrp_ios_mount.md +++ b/docs/cmd/hrp_ios_mount.md @@ -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 diff --git a/docs/cmd/hrp_ios_ps.md b/docs/cmd/hrp_ios_ps.md index 9173a89f..5696a24e 100644 --- a/docs/cmd/hrp_ios_ps.md +++ b/docs/cmd/hrp_ios_ps.md @@ -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 diff --git a/docs/cmd/hrp_ios_reboot.md b/docs/cmd/hrp_ios_reboot.md index 81d41d98..9feb9fb4 100644 --- a/docs/cmd/hrp_ios_reboot.md +++ b/docs/cmd/hrp_ios_reboot.md @@ -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 diff --git a/docs/cmd/hrp_ios_tunnel.md b/docs/cmd/hrp_ios_tunnel.md index 51fa5033..b80eeedc 100644 --- a/docs/cmd/hrp_ios_tunnel.md +++ b/docs/cmd/hrp_ios_tunnel.md @@ -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 diff --git a/docs/cmd/hrp_ios_uninstall.md b/docs/cmd/hrp_ios_uninstall.md index ad4e1247..d2ebcb5a 100644 --- a/docs/cmd/hrp_ios_uninstall.md +++ b/docs/cmd/hrp_ios_uninstall.md @@ -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 diff --git a/docs/cmd/hrp_ios_xctest.md b/docs/cmd/hrp_ios_xctest.md index 50ea73dd..001fb2e1 100644 --- a/docs/cmd/hrp_ios_xctest.md +++ b/docs/cmd/hrp_ios_xctest.md @@ -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 diff --git a/docs/cmd/hrp_mcp-server.md b/docs/cmd/hrp_mcp-server.md index b066c165..f906af4b 100644 --- a/docs/cmd/hrp_mcp-server.md +++ b/docs/cmd/hrp_mcp-server.md @@ -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 diff --git a/docs/cmd/hrp_mcphost.md b/docs/cmd/hrp_mcphost.md index c0d5dff1..bebdbc4f 100644 --- a/docs/cmd/hrp_mcphost.md +++ b/docs/cmd/hrp_mcphost.md @@ -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 diff --git a/docs/cmd/hrp_pytest.md b/docs/cmd/hrp_pytest.md index ecb007ea..a2b0a11c 100644 --- a/docs/cmd/hrp_pytest.md +++ b/docs/cmd/hrp_pytest.md @@ -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 diff --git a/docs/cmd/hrp_report.md b/docs/cmd/hrp_report.md index 9d713483..d781cea7 100644 --- a/docs/cmd/hrp_report.md +++ b/docs/cmd/hrp_report.md @@ -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 diff --git a/docs/cmd/hrp_run.md b/docs/cmd/hrp_run.md index 323493b0..12c2cb62 100644 --- a/docs/cmd/hrp_run.md +++ b/docs/cmd/hrp_run.md @@ -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 diff --git a/docs/cmd/hrp_server.md b/docs/cmd/hrp_server.md index 7edb4106..da60a80d 100644 --- a/docs/cmd/hrp_server.md +++ b/docs/cmd/hrp_server.md @@ -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 diff --git a/docs/cmd/hrp_startproject.md b/docs/cmd/hrp_startproject.md index 5826d901..e0605b43 100644 --- a/docs/cmd/hrp_startproject.md +++ b/docs/cmd/hrp_startproject.md @@ -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 diff --git a/docs/cmd/hrp_wiki.md b/docs/cmd/hrp_wiki.md index 53e6e4c9..042fd8c0 100644 --- a/docs/cmd/hrp_wiki.md +++ b/docs/cmd/hrp_wiki.md @@ -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 diff --git a/examples/game/2048/game_2048.json b/examples/game/2048/game_2048.json new file mode 100644 index 00000000..4eb28516 --- /dev/null +++ b/examples/game/2048/game_2048.json @@ -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 + } + } + ] + } + } + ] +} diff --git a/examples/game/2048/main_test.go b/examples/game/2048/main_test.go new file mode 100644 index 00000000..304fd3a1 --- /dev/null +++ b/examples/game/2048/main_test.go @@ -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) +} diff --git a/examples/game/llk/game_llk.json b/examples/game/llk/game_llk.json new file mode 100644 index 00000000..07e39089 --- /dev/null +++ b/examples/game/llk/game_llk.json @@ -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 + } + } + ] + } + } + ] +} diff --git a/examples/game/llk/main_test.go b/examples/game/llk/main_test.go index 981318ba..a7eb28fb 100644 --- a/examples/game/llk/main_test.go +++ b/examples/game/llk/main_test.go @@ -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 { diff --git a/examples/uitest/demo_ios_wda_log.json b/examples/uitest/demo_ios_wda_log.json index b89e4fbe..32a84e8e 100644 --- a/examples/uitest/demo_ios_wda_log.json +++ b/examples/uitest/demo_ios_wda_log.json @@ -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": "点击搜索" + } } ] } diff --git a/examples/uitest/demo_ios_wda_log_test.go b/examples/uitest/demo_ios_wda_log_test.go index 30407c0d..35a7c8de 100644 --- a/examples/uitest/demo_ios_wda_log_test.go +++ b/examples/uitest/demo_ios_wda_log_test.go @@ -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("点击搜索")), }, } diff --git a/internal/version/VERSION b/internal/version/VERSION index 8bcffb32..54ae7b48 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2506261443 +v5.0.0-250627 diff --git a/report.go b/report.go index a7aa5fe8..3f304175 100644 --- a/report.go +++ b/report.go @@ -12,11 +12,12 @@ import ( "strings" "time" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v5/internal/builtin" "github.com/httprunner/httprunner/v5/uixt" "github.com/httprunner/httprunner/v5/uixt/option" - "github.com/pkg/errors" - "github.com/rs/zerolog/log" ) // GenerateHTMLReportFromFiles is a convenience function to generate HTML report @@ -41,6 +42,7 @@ type HTMLReportGenerator struct { ReportDir string SummaryContent string // Raw summary.json content for download LogContent string // Raw hrp.log content for download + CaseContent string // Raw case.json content for display } // LogEntry represents a single log entry @@ -72,6 +74,11 @@ func NewHTMLReportGenerator(summaryFile, logFile string) (*HTMLReportGenerator, } } + // Load case.json data if exists + if err := generator.loadCaseData(); err != nil { + log.Warn().Err(err).Msg("failed to load case data, continuing without case display") + } + return generator, nil } @@ -208,7 +215,24 @@ func (g *HTMLReportGenerator) loadLogData() error { return scanner.Err() } -// getStepLogs filters log entries for a specific test step based on step boundaries +// loadCaseData loads test case data from case.json file +func (g *HTMLReportGenerator) loadCaseData() error { + caseFile := filepath.Join(g.ReportDir, "case.json") + if !builtin.FileExists(caseFile) { + return nil // case.json is optional + } + + data, err := os.ReadFile(caseFile) + if err != nil { + return err + } + + // Store the case content for display + g.CaseContent = string(data) + return nil +} + +// getStepLogs filters log entries for a specific test step using prefix matching and time range filtering func (g *HTMLReportGenerator) getStepLogs(stepName string, startTime int64, elapsed int64) []LogEntry { if len(g.LogData) == 0 { return nil @@ -217,8 +241,19 @@ func (g *HTMLReportGenerator) getStepLogs(stepName string, startTime int64, elap var stepLogs []LogEntry var inCurrentStep bool = false - // Simple approach: use step start/end markers for precise boundaries + // Calculate step end time (startTime + elapsed, both in milliseconds) + endTime := startTime + elapsed + + // Convert step times to time.Time for comparison + // The startTime from step result is in milliseconds timestamp + stepStartTime := time.UnixMilli(startTime) + stepEndTime := time.UnixMilli(endTime) + + // Use step start/end markers with prefix matching for precise boundaries for _, logEntry := range g.LogData { + // Parse log entry timestamp for time range validation + logTime, timeParseErr := g.parseLogTime(logEntry.Time) + // Check for step boundaries to control inclusion if logEntry.Message == RUN_STEP_START { if stepFieldValue, exists := logEntry.Fields["step"].(string); exists { @@ -228,7 +263,7 @@ func (g *HTMLReportGenerator) getStepLogs(stepName string, startTime int64, elap stepLogs = append(stepLogs, logEntry) continue } else if inCurrentStep { - // This is a different step starting, we're done + // This is a different step starting, we're done with current step break } } @@ -240,31 +275,30 @@ func (g *HTMLReportGenerator) getStepLogs(stepName string, startTime int64, elap if strings.HasPrefix(stepName, stepFieldValue) { stepLogs = append(stepLogs, logEntry) inCurrentStep = false - continue + break // End of current step, stop processing } } } - // Only include logs when we're in the current step + // Include logs when we're in the current step AND within the time range if inCurrentStep { - stepLogs = append(stepLogs, logEntry) + // Apply time range filtering if time parsing succeeded + if timeParseErr == nil { + // Only include logs within the step time range + if (logTime.Equal(stepStartTime) || logTime.After(stepStartTime)) && + (logTime.Equal(stepEndTime) || logTime.Before(stepEndTime)) { + stepLogs = append(stepLogs, logEntry) + } + } else { + // If time parsing failed, include all logs in the step boundary + stepLogs = append(stepLogs, logEntry) + } } } - // Sort logs by time, then by original index for stable ordering + // Sort logs by original index to maintain chronological order sort.Slice(stepLogs, func(i, j int) bool { - timeI, errI := g.parseLogTime(stepLogs[i].Time) - timeJ, errJ := g.parseLogTime(stepLogs[j].Time) - - if errI != nil || errJ != nil { - return stepLogs[i].LogIndex < stepLogs[j].LogIndex - } - - if timeI.Equal(timeJ) { - // For same timestamps, use original log index to maintain order - return stepLogs[i].LogIndex < stepLogs[j].LogIndex - } - return timeI.Before(timeJ) + return stepLogs[i].LogIndex < stepLogs[j].LogIndex }) return stepLogs @@ -514,16 +548,20 @@ func (g *HTMLReportGenerator) GenerateReport(outputFile string) error { "getLogContentBase64": func() string { return base64.StdEncoding.EncodeToString([]byte(g.LogContent)) }, + "getCaseContentBase64": func() string { + return base64.StdEncoding.EncodeToString([]byte(g.CaseContent)) + }, "safeHTML": func(s string) template.HTML { return template.HTML(s) }, - "toJSON": func(v any) string { + "toJSONFormatted": func(v any) string { var buf strings.Builder encoder := json.NewEncoder(&buf) encoder.SetEscapeHTML(false) + encoder.SetIndent("", " ") _ = encoder.Encode(v) - result := buf.String() - return strings.TrimSpace(result) + result := strings.TrimSpace(buf.String()) + return result }, "add": func(a, b int) int { return a + b }, "base": filepath.Base, @@ -548,6 +586,20 @@ func (g *HTMLReportGenerator) GenerateReport(outputFile string) error { // If not JSON or no thought field, return original content return content }, + "formatBodyContent": func(content string) string { + // Try to parse as JSON to format + var data interface{} + if err := json.Unmarshal([]byte(content), &data); err == nil { + var buf strings.Builder + encoder := json.NewEncoder(&buf) + encoder.SetEscapeHTML(false) + encoder.SetIndent("", " ") + _ = encoder.Encode(data) + return strings.TrimSpace(buf.String()) + } + // If not JSON, return original content + return content + }, } // Parse template @@ -1336,9 +1388,12 @@ const htmlTemplate = ` margin: 2px 0; font-family: monospace; font-size: 0.7em; - max-height: 60px; + max-height: 80px; overflow-y: auto; word-break: break-all; + white-space: nowrap; + overflow-x: auto; + line-height: 1.3; } .model-output-compact { @@ -1468,12 +1523,6 @@ const htmlTemplate = ` font-weight: 600; } - .screenshots-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 10px; - } - .screenshots-horizontal { display: flex; gap: 15px; @@ -1867,26 +1916,6 @@ const htmlTemplate = ` overflow: hidden; } - .controls { - text-align: center; - margin-bottom: 20px; - } - - .controls button { - background: #007bff; - color: white; - border: none; - padding: 8px 16px; - border-radius: 4px; - margin: 0 5px; - cursor: pointer; - transition: background-color 0.3s; - } - - .controls button:hover { - background: #0056b3; - } - /* Modal styles */ .modal { display: none; @@ -1908,6 +1937,167 @@ const htmlTemplate = ` object-fit: contain; } + .json-modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.8); + overflow: auto; + } + + .json-modal-content { + background-color: #fefefe; + margin: 2% auto; + padding: 0; + border: none; + border-radius: 12px; + width: 90%; + max-width: 1000px; + max-height: 90%; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); + display: flex; + flex-direction: column; + } + + .json-modal-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 20px 30px; + border-radius: 12px 12px 0 0; + display: flex; + justify-content: space-between; + align-items: center; + } + + .json-modal-title { + font-size: 1.5em; + font-weight: 600; + margin: 0; + } + + .json-modal-body { + padding: 0; + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; + } + + .json-content { + background: #f8f9fa; + margin: 0; + padding: 20px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; + font-size: 13px; + line-height: 1.5; + color: #333; + overflow: auto; + flex: 1; + white-space: pre; + border-radius: 0 0 12px 12px; + } + + /* JSON Syntax Highlighting */ + .json-key { + color: #0066cc; + font-weight: bold; + } + + .json-string { + color: #22863a; + } + + .json-number { + color: #e36209; + } + + .json-boolean { + color: #d73a49; + font-weight: bold; + } + + .json-null { + color: #6f42c1; + font-weight: bold; + } + + .json-punctuation { + color: #24292e; + } + + .json-brace { + color: #586069; + font-weight: bold; + } + + .json-bracket { + color: #586069; + font-weight: bold; + } + + /* Inline JSON highlighting for smaller displays */ + .json-inline .json-key { + color: #0066cc; + font-weight: 600; + } + + .json-inline .json-string { + color: #22863a; + } + + .json-inline .json-number { + color: #e36209; + } + + .json-inline .json-boolean { + color: #d73a49; + font-weight: 600; + } + + .json-inline .json-null { + color: #6f42c1; + font-weight: 600; + } + + .json-toolbar { + background: #e9ecef; + padding: 10px 20px; + border-top: 1px solid #dee2e6; + display: flex; + justify-content: space-between; + align-items: center; + } + + .json-toolbar button { + background: #007bff; + color: white; + border: none; + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 0.9em; + transition: background-color 0.3s; + } + + .json-toolbar button:hover { + background: #0056b3; + } + + .json-toolbar .copy-status { + font-size: 0.9em; + color: #28a745; + opacity: 0; + transition: opacity 0.3s; + } + + .json-toolbar .copy-status.show { + opacity: 1; + } + .close { position: absolute; top: 15px; @@ -1919,6 +2109,22 @@ const htmlTemplate = ` cursor: pointer; } + .json-close { + color: white; + font-size: 30px; + font-weight: bold; + cursor: pointer; + transition: color 0.3s; + background: none; + border: none; + padding: 0; + line-height: 1; + } + + .json-close:hover { + color: #ffcccc; + } + .close:hover, .close:focus { color: #bbb; @@ -2139,21 +2345,6 @@ const htmlTemplate = ` font-size: 0.95em; } - .action-output { - background: #f8f9fa; - border: 2px solid #6f42c1; - border-radius: 6px; - padding: 10px; - font-size: 0.85em; - max-height: 120px; - overflow-y: auto; - white-space: pre-wrap; - word-wrap: break-word; - color: #495057; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - line-height: 1.4; - } - .action-session-data { margin-top: 15px; padding: 15px; @@ -2186,19 +2377,25 @@ const htmlTemplate = `
Start Time: {{.Time.StartAt.Format "2006-01-02 15:04:05"}}
-
-
📥 Download
-
- - +
+
📥 View & Download
+
+ {{if getCaseContentBase64}} + + {{end}} + + +
-
@@ -2381,7 +2578,7 @@ const htmlTemplate = `
🔧 Tool Calls: {{$planning.ToolCallsCount}}
{{end}} {{if $planning.ActionNames}} -
🎯 Actions: {{safeHTML (toJSON $planning.ActionNames)}}
+
🎯 Actions: {{toJSONFormatted $planning.ActionNames}}
{{end}} @@ -2403,7 +2600,9 @@ const htmlTemplate = ` {{if $subAction.Error}}{{else}}{{end}} {{if $subAction.Arguments}} -
{{safeHTML (toJSON $subAction.Arguments)}}
+
+ {{toJSONFormatted $subAction.Arguments}} +
{{end}} {{if $subAction.Requests}}
@@ -2511,7 +2710,7 @@ const htmlTemplate = ` {{/* Display structured data for query results */}} {{if $action.AIResult.QueryResult.Data}}
📥 Structured Data:
-
{{safeHTML (toJSON $action.AIResult.QueryResult.Data)}}
+
{{toJSONFormatted $action.AIResult.QueryResult.Data}}
{{end}} {{else if eq $action.AIResult.Type "action"}} {{if $action.AIResult.PlanningResult.ModelName}} @@ -2693,7 +2892,9 @@ const htmlTemplate = ` {{end}}
{{if $logEntry.Fields}} - + {{end}} {{end}} @@ -2714,10 +2915,71 @@ const htmlTemplate = ` + +
+
+
+

📋 Test Case JSON

+ +
+
+
+
+ + +
+ ✅ Copied! +
+

+            
+
+
+ + +
+
+
+

📄 Summary JSON

+ +
+
+
+
+ + +
+ ✅ Copied! +
+

+            
+
+
+ + +
+
+
+

📋 Log Content

+ +
+
+
+
+ + +
+ ✅ Copied! +
+

+            
+
+
+